mirror of
https://github.com/Grace-Solutions/Invoke-HTTPBootBiosConfiguration.git
synced 2026-09-12 05:49:05 +00:00
c3e75ceb5a
- Rename OSD-ScriptTemplate to Invoke-HTTPBootBiosConfiguration (script and bootstrapper) - Configure Dell HTTPS boot via CCTK: enable HttpsBoot, set ManualMode, delete existing HttpBootProfile (avoids stale URL not applying), apply generated profile, and verify the applied URL via --Get - Build the HttpBootProfile XML with XmlDocument/XmlWriter; embed the dynamically downloaded Let's Encrypt ISRG Root X1 root certificate; IntegrityInfo emitted with an intentionally empty digest (no hashing - values only) - Default boot image file name snponly_x64.efi is appended when the boot URL does not end with a file name (trailing slash tolerant) - Add Invoke-ToolStaging toolkit function: downloads and extracts tools into Toolkit\Tools (All/X86/X64/ARM64) regardless of manufacturer, so staged bits travel with the script folder and repeat executions use cached bits; supports RawFile, Archive (.zip/.7z), MSI (administrative extraction), and DellUpdatePackage (7z payload carved by binary signature, extracted with portable 7zr.exe) - Add ARM64 support to the toolkit OS architecture calculation - Automatic proxy resolution for downloads: user WinINET proxy, then machine WinHTTP proxy (binary WinHttpSettings parsing incl. per-protocol lists and bypass list), then direct - Windows PE support; WinPE-MSI only needed for the direct DUP path (stage once from a full OS instead) - --ValSetupPwd appended only on modification commands when a setup password is supplied; verified CCTK ignores it when no BIOS password is installed, so one command line works fleet-wide - Git ignore staged tool binaries (cannot be redistributed); add README, Docs (CCTK acquisition, HttpBootProfile reference, WinPE guide), and PowerShell authoring rules under .claude/rules Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
625 lines
41 KiB
PowerShell
625 lines
41 KiB
PowerShell
#region Invoke-ToolStaging
|
|
Function Invoke-ToolStaging
|
|
{
|
|
<#
|
|
.SYNOPSIS
|
|
Dynamically stages external tool binaries into the toolkit tools directory so that they travel with the script and repeat executions use the cached bits.
|
|
|
|
.DESCRIPTION
|
|
Some vendor binaries cannot be redistributed with this repository, so they are downloaded and extracted at execution time instead. This function downloads each defined tool, extracts it when necessary, and places the binaries into the toolkit tools directory structure (All, X86, X64, ARM64). Once staged, the binaries travel with the script folder (for example on a deployment share), so subsequent executions on any device use the cached bits without downloading anything.
|
|
|
|
The following tool types are supported:
|
|
|
|
RawFile - The downloaded file is placed directly at the destination (no extraction).
|
|
Archive - A .zip archive (extracted with the built in compression classes) or a .7z archive (extracted with the 7-Zip console executable).
|
|
MSI - An MSI package expanded by using an MSI administrative extraction (msiexec /a), which produces the installed file tree without installing anything.
|
|
DellUpdatePackage - A Dell Update Package (DUP) executable. The embedded 7-Zip payload is carved out of the package by binary signature, extracted with the 7-Zip console executable, and the resulting MSI is expanded by using an MSI administrative extraction.
|
|
|
|
.PARAMETER ToolList
|
|
One or more tool definitions. Each definition is an ordered dictionary with the following properties:
|
|
|
|
Enabled - Boolean. Disabled definitions are skipped.
|
|
Name - The tool name. Used for logging and for the working folder name.
|
|
Type - RawFile, Archive, MSI, or DellUpdatePackage.
|
|
DownloadURL - The URL the tool content is downloaded from.
|
|
DestinationMappingTable - An ordered dictionary that maps content within the extracted tree to a destination relative to the tools directory. The key is a folder or file name to locate within the extracted tree (or "." for the downloaded file itself), and the value is the destination path relative to the tools directory. Mappings whose source cannot be located are logged and skipped, which allows a single definition to include architecture folders that are not present within every source.
|
|
CompletionTestPathList - One or more paths relative to the tools directory. When ANY of them exists, the tool is considered already staged and is skipped (unless the Force parameter is specified).
|
|
|
|
.PARAMETER ToolsDirectory
|
|
The root toolkit tools directory that the binaries are staged into (the folder containing the All, X86, X64, and ARM64 subdirectories).
|
|
|
|
.PARAMETER StagingDirectory
|
|
The working directory used for downloads and extraction. Keep this path short, because MSI administrative extractions can fail with "path too long" errors when the working path is deep. The working content for each tool is removed after it has been staged successfully.
|
|
|
|
.PARAMETER SevenZipExecutablePath
|
|
The path to the 7-Zip console executable (7zr.exe). Required to extract Dell Update Package payloads and .7z archives. Defaults to "All\7-Zip\7zr.exe" within the tools directory, so a RawFile definition that stages 7-Zip should be placed before any definition that requires it.
|
|
|
|
.PARAMETER WebProxy
|
|
An optional web proxy that is applied to every download. When not specified, no proxy is used.
|
|
|
|
.PARAMETER Force
|
|
Stages each tool even when its completion test paths indicate that it has already been staged.
|
|
|
|
.PARAMETER ContinueOnError
|
|
Continue with the remaining tool definitions instead of throwing a terminating error when a tool cannot be staged.
|
|
|
|
.EXAMPLE
|
|
$ToolStagingList = New-Object -TypeName 'System.Collections.Generic.List[System.Collections.IDictionary]'
|
|
|
|
$ToolDefinition = New-Object -TypeName 'System.Collections.Specialized.OrderedDictionary'
|
|
$ToolDefinition.Enabled = $True
|
|
$ToolDefinition.Name = 'SevenZip'
|
|
$ToolDefinition.Type = 'RawFile'
|
|
$ToolDefinition.DownloadURL = [System.URI]'https://www.7-zip.org/a/7zr.exe'
|
|
$ToolDefinition.DestinationMappingTable = New-Object -TypeName 'System.Collections.Specialized.OrderedDictionary'
|
|
$ToolDefinition.DestinationMappingTable['.'] = [System.IO.Path]::Combine('All', '7-Zip', '7zr.exe')
|
|
$ToolDefinition.CompletionTestPathList = New-Object -TypeName 'System.Collections.Generic.List[String]'
|
|
$ToolDefinition.CompletionTestPathList.Add([System.IO.Path]::Combine('All', '7-Zip', '7zr.exe'))
|
|
$ToolStagingList.Add($ToolDefinition)
|
|
|
|
$InvokeToolStagingResult = Invoke-ToolStaging -ToolList ($ToolStagingList.ToArray()) -ToolsDirectory ($ToolsDirectory) -Verbose
|
|
|
|
.NOTES
|
|
The download logic always sends a user agent header, because some download servers (such as dl.dell.com) reject anonymous requests.
|
|
|
|
.LINK
|
|
https://www.dell.com/support/kbdoc/en-us/000178000/dell-command-configure
|
|
#>
|
|
|
|
[CmdletBinding()]
|
|
Param
|
|
(
|
|
[Parameter(Mandatory=$True)]
|
|
[ValidateNotNullOrEmpty()]
|
|
[Alias('TL')]
|
|
[System.Collections.IDictionary[]]$ToolList,
|
|
|
|
[Parameter(Mandatory=$True)]
|
|
[ValidateNotNullOrEmpty()]
|
|
[Alias('TD')]
|
|
[System.IO.DirectoryInfo]$ToolsDirectory,
|
|
|
|
[Parameter(Mandatory=$False)]
|
|
[ValidateNotNullOrEmpty()]
|
|
[Alias('SD')]
|
|
[System.IO.DirectoryInfo]$StagingDirectory,
|
|
|
|
[Parameter(Mandatory=$False)]
|
|
[ValidateNotNullOrEmpty()]
|
|
[Alias('SZEP')]
|
|
[System.IO.FileInfo]$SevenZipExecutablePath,
|
|
|
|
[Parameter(Mandatory=$False)]
|
|
[ValidateNotNullOrEmpty()]
|
|
[Alias('WP')]
|
|
[System.Net.IWebProxy]$WebProxy,
|
|
|
|
[Parameter(Mandatory=$False)]
|
|
[Alias('F')]
|
|
[Switch]$Force,
|
|
|
|
[Parameter(Mandatory=$False)]
|
|
[Alias('COE')]
|
|
[Switch]$ContinueOnError
|
|
)
|
|
|
|
Try
|
|
{
|
|
$ErrorActionPreference = 'Stop'
|
|
|
|
[String]$CmdletName = $MyInvocation.MyCommand.Name
|
|
|
|
$WriteLogMessage.Invoke(0, @("Function `'$($CmdletName)`' is beginning. Please Wait..."))
|
|
|
|
#region Set default parameter values
|
|
Switch ($True)
|
|
{
|
|
{([System.String]::IsNullOrEmpty($StagingDirectory) -eq $True) -or ([System.String]::IsNullOrWhiteSpace($StagingDirectory) -eq $True)}
|
|
{
|
|
[System.IO.DirectoryInfo]$StagingDirectory = [System.IO.Path]::Combine("$($Env:Windir)", 'Temp', 'ToolStaging')
|
|
}
|
|
|
|
{([System.String]::IsNullOrEmpty($SevenZipExecutablePath) -eq $True) -or ([System.String]::IsNullOrWhiteSpace($SevenZipExecutablePath) -eq $True)}
|
|
{
|
|
[System.IO.FileInfo]$SevenZipExecutablePath = [System.IO.Path]::Combine("$($ToolsDirectory.FullName)", 'All', '7-Zip', '7zr.exe')
|
|
}
|
|
}
|
|
#endregion
|
|
|
|
#region Define the internal download scriptblock
|
|
[ScriptBlock]$DownloadToolFile = {
|
|
Param
|
|
(
|
|
[System.URI]$SourceURL,
|
|
[System.IO.FileInfo]$DestinationPath
|
|
)
|
|
|
|
$WriteLogMessage.Invoke(0, @("Attempting to download `"$($SourceURL.AbsoluteUri)`" to `"$($DestinationPath.FullName)`". Please Wait..."))
|
|
|
|
Switch ([System.IO.Directory]::Exists($DestinationPath.Directory.FullName))
|
|
{
|
|
{($_ -eq $False)}
|
|
{
|
|
$Null = [System.IO.Directory]::CreateDirectory($DestinationPath.Directory.FullName)
|
|
}
|
|
}
|
|
|
|
$WebClient = New-Object -TypeName 'System.Net.WebClient'
|
|
|
|
Try
|
|
{
|
|
$WebClient.UseDefaultCredentials = $True
|
|
|
|
Switch ($Null -ine $WebProxy)
|
|
{
|
|
{($_ -eq $True)}
|
|
{
|
|
$WebClient.Proxy = $WebProxy
|
|
}
|
|
|
|
Default
|
|
{
|
|
$WebClient.Proxy = $Null
|
|
}
|
|
}
|
|
|
|
$Null = $WebClient.Headers.Add('User-Agent', 'Mozilla/5.0 (Windows NT 10.0; Win64; x64) ToolStaging')
|
|
|
|
$Null = $WebClient.DownloadFile($SourceURL.AbsoluteUri, $DestinationPath.FullName)
|
|
|
|
$WriteLogMessage.Invoke(0, @("The download completed successfully. [Size: $([System.Math]::Round((Get-Item -Path $DestinationPath.FullName).Length / 1MB, 2)) MB]"))
|
|
}
|
|
Finally
|
|
{
|
|
$Null = $WebClient.Dispose()
|
|
}
|
|
}
|
|
#endregion
|
|
|
|
#region Define the MSI administrative extraction scriptblock
|
|
[ScriptBlock]$ExpandMSIPackage = {
|
|
Param
|
|
(
|
|
[System.IO.FileInfo]$MSIPackagePath,
|
|
[System.IO.DirectoryInfo]$DestinationDirectory
|
|
)
|
|
|
|
$MSIExecCommandObject = Try {Get-Command -Name 'msiexec.exe' -ErrorAction SilentlyContinue} Catch {$Null}
|
|
|
|
Switch ($Null -ieq $MSIExecCommandObject)
|
|
{
|
|
{($_ -eq $True)}
|
|
{
|
|
Throw 'The msiexec.exe executable could not be located, so the MSI cannot be expanded. Within Windows PE, either add the WinPE-MSI optional component to the boot image, or stage the tools by executing the script once from a full Windows operating system so that the staged binaries travel with the script folder.'
|
|
}
|
|
}
|
|
|
|
$WriteLogMessage.Invoke(0, @("Attempting to expand `"$($MSIPackagePath.FullName)`" into `"$($DestinationDirectory.FullName)`". Please Wait..."))
|
|
|
|
$StartProcessWithOutputParameters = New-Object -TypeName 'System.Collections.Specialized.OrderedDictionary'
|
|
$StartProcessWithOutputParameters.FilePath = "$($MSIExecCommandObject.Path)"
|
|
$StartProcessWithOutputParameters.WorkingDirectory = "$($MSIPackagePath.Directory.FullName)"
|
|
$StartProcessWithOutputParameters.ArgumentList = New-Object -TypeName 'System.Collections.Generic.List[String]'
|
|
$StartProcessWithOutputParameters.ArgumentList.Add('/a')
|
|
$StartProcessWithOutputParameters.ArgumentList.Add("`"$($MSIPackagePath.FullName)`"")
|
|
$StartProcessWithOutputParameters.ArgumentList.Add("TARGETDIR=`"$($DestinationDirectory.FullName)`"")
|
|
$StartProcessWithOutputParameters.ArgumentList.Add('/qn')
|
|
$StartProcessWithOutputParameters.AcceptableExitCodeList = New-Object -TypeName 'System.Collections.Generic.List[String]'
|
|
$StartProcessWithOutputParameters.AcceptableExitCodeList.Add('0')
|
|
$StartProcessWithOutputParameters.CreateNoWindow = $True
|
|
$StartProcessWithOutputParameters.ExecutionTimeout = [System.TimeSpan]::FromMinutes(10)
|
|
$StartProcessWithOutputParameters.LogOutput = $True
|
|
$StartProcessWithOutputParameters.Verbose = $True
|
|
|
|
$Null = Start-ProcessWithOutput @StartProcessWithOutputParameters
|
|
}
|
|
#endregion
|
|
|
|
#region Define the 7-Zip extraction scriptblock
|
|
[ScriptBlock]$ExpandSevenZipArchive = {
|
|
Param
|
|
(
|
|
[System.IO.FileInfo]$ArchivePath,
|
|
[System.IO.DirectoryInfo]$DestinationDirectory
|
|
)
|
|
|
|
Switch ([System.IO.File]::Exists($SevenZipExecutablePath.FullName))
|
|
{
|
|
{($_ -eq $False)}
|
|
{
|
|
Throw "The 7-Zip console executable does not exist at `"$($SevenZipExecutablePath.FullName)`". Ensure that a RawFile tool definition that stages 7-Zip is placed before any definition that requires it."
|
|
}
|
|
}
|
|
|
|
$WriteLogMessage.Invoke(0, @("Attempting to extract `"$($ArchivePath.FullName)`" into `"$($DestinationDirectory.FullName)`". Please Wait..."))
|
|
|
|
#An exit code of 1 is a 7-Zip warning (such as trailing bytes after the end of a carved archive) and is acceptable.
|
|
$StartProcessWithOutputParameters = New-Object -TypeName 'System.Collections.Specialized.OrderedDictionary'
|
|
$StartProcessWithOutputParameters.FilePath = "$($SevenZipExecutablePath.FullName)"
|
|
$StartProcessWithOutputParameters.WorkingDirectory = "$($ArchivePath.Directory.FullName)"
|
|
$StartProcessWithOutputParameters.ArgumentList = New-Object -TypeName 'System.Collections.Generic.List[String]'
|
|
$StartProcessWithOutputParameters.ArgumentList.Add('x')
|
|
$StartProcessWithOutputParameters.ArgumentList.Add("`"$($ArchivePath.FullName)`"")
|
|
$StartProcessWithOutputParameters.ArgumentList.Add("`"-o$($DestinationDirectory.FullName)`"")
|
|
$StartProcessWithOutputParameters.ArgumentList.Add('-y')
|
|
$StartProcessWithOutputParameters.AcceptableExitCodeList = New-Object -TypeName 'System.Collections.Generic.List[String]'
|
|
$StartProcessWithOutputParameters.AcceptableExitCodeList.Add('0')
|
|
$StartProcessWithOutputParameters.AcceptableExitCodeList.Add('1')
|
|
$StartProcessWithOutputParameters.CreateNoWindow = $True
|
|
$StartProcessWithOutputParameters.ExecutionTimeout = [System.TimeSpan]::FromMinutes(10)
|
|
$StartProcessWithOutputParameters.LogOutput = $True
|
|
$StartProcessWithOutputParameters.Verbose = $True
|
|
|
|
$Null = Start-ProcessWithOutput @StartProcessWithOutputParameters
|
|
}
|
|
#endregion
|
|
|
|
$OutputObjectList = New-Object -TypeName 'System.Collections.Generic.List[PSObject]'
|
|
|
|
[Int]$ToolStagingErrorCount = 0
|
|
|
|
:ToolListLoop For ($ToolListIndex = 0; $ToolListIndex -lt $ToolList.Count; $ToolListIndex++)
|
|
{
|
|
Try
|
|
{
|
|
$ToolDefinition = $ToolList[$ToolListIndex]
|
|
|
|
$OutputObjectProperties = New-Object -TypeName 'System.Collections.Specialized.OrderedDictionary'
|
|
$OutputObjectProperties.Name = "$($ToolDefinition.Name)"
|
|
$OutputObjectProperties.Staged = $False
|
|
$OutputObjectProperties.FromCache = $False
|
|
$OutputObjectProperties.DestinationPathList = New-Object -TypeName 'System.Collections.Generic.List[String]'
|
|
|
|
Switch ($ToolDefinition.Enabled)
|
|
{
|
|
{($_ -eq $False)}
|
|
{
|
|
$WriteLogMessage.Invoke(0, @("Tool $($ToolListIndex + 1) of $($ToolList.Count) `"$($ToolDefinition.Name)`" is disabled and will be skipped."))
|
|
|
|
$OutputObjectList.Add((New-Object -TypeName 'PSObject' -Property ($OutputObjectProperties)))
|
|
|
|
Continue ToolListLoop
|
|
}
|
|
}
|
|
|
|
$WriteLogMessage.Invoke(0, @("Tool $($ToolListIndex + 1) of $($ToolList.Count): Attempting to stage `"$($ToolDefinition.Name)`" [Type: $($ToolDefinition.Type)]. Please Wait..."))
|
|
|
|
#region Determine whether the tool has already been staged
|
|
[Boolean]$ToolIsCached = $False
|
|
|
|
Switch (($Force.IsPresent -eq $False) -and ($Null -ine $ToolDefinition.CompletionTestPathList))
|
|
{
|
|
{($_ -eq $True)}
|
|
{
|
|
:CompletionTestPathLoop ForEach ($CompletionTestPath In $ToolDefinition.CompletionTestPathList)
|
|
{
|
|
[String]$CompletionTestFullPath = [System.IO.Path]::Combine("$($ToolsDirectory.FullName)", "$($CompletionTestPath)")
|
|
|
|
Switch (([System.IO.File]::Exists($CompletionTestFullPath) -eq $True) -or ([System.IO.Directory]::Exists($CompletionTestFullPath) -eq $True))
|
|
{
|
|
{($_ -eq $True)}
|
|
{
|
|
[Boolean]$ToolIsCached = $True
|
|
|
|
$WriteLogMessage.Invoke(0, @("Tool `"$($ToolDefinition.Name)`" has already been staged. The cached bits will be used. [Path: $($CompletionTestFullPath)]"))
|
|
|
|
Break CompletionTestPathLoop
|
|
}
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
Switch ($ToolIsCached)
|
|
{
|
|
{($_ -eq $True)}
|
|
{
|
|
$OutputObjectProperties.Staged = $True
|
|
$OutputObjectProperties.FromCache = $True
|
|
|
|
$OutputObjectList.Add((New-Object -TypeName 'PSObject' -Property ($OutputObjectProperties)))
|
|
|
|
Continue ToolListLoop
|
|
}
|
|
}
|
|
#endregion
|
|
|
|
#region Download the tool content
|
|
[System.URI]$ToolDownloadURL = $ToolDefinition.DownloadURL
|
|
|
|
[System.IO.DirectoryInfo]$ToolWorkingDirectory = [System.IO.Path]::Combine("$($StagingDirectory.FullName)", "$($ToolDefinition.Name)")
|
|
|
|
[System.IO.FileInfo]$ToolDownloadPath = [System.IO.Path]::Combine("$($ToolWorkingDirectory.FullName)", [System.IO.Path]::GetFileName($ToolDownloadURL.LocalPath))
|
|
|
|
Switch ([System.IO.File]::Exists($ToolDownloadPath.FullName))
|
|
{
|
|
{($_ -eq $True)}
|
|
{
|
|
$WriteLogMessage.Invoke(0, @("The tool content has already been downloaded to `"$($ToolDownloadPath.FullName)`". The existing file will be reused."))
|
|
}
|
|
|
|
Default
|
|
{
|
|
$Null = $DownloadToolFile.InvokeReturnAsIs($ToolDownloadURL, $ToolDownloadPath)
|
|
}
|
|
}
|
|
#endregion
|
|
|
|
#region Extract the tool content based on the tool type
|
|
[System.IO.DirectoryInfo]$ToolExtractionDirectory = [System.IO.Path]::Combine("$($ToolWorkingDirectory.FullName)", 'Extracted')
|
|
|
|
Switch -Regex ("$($ToolDefinition.Type)")
|
|
{
|
|
'(^RawFile$)'
|
|
{
|
|
#No extraction is necessary. The destination mapping key "." refers to the downloaded file itself.
|
|
}
|
|
|
|
'(^Archive$)'
|
|
{
|
|
Switch ([System.IO.Path]::GetExtension($ToolDownloadPath.FullName).ToLower())
|
|
{
|
|
{($_ -iin @('.zip'))}
|
|
{
|
|
$WriteLogMessage.Invoke(0, @("Attempting to extract the ZIP archive `"$($ToolDownloadPath.FullName)`" into `"$($ToolExtractionDirectory.FullName)`". Please Wait..."))
|
|
|
|
$Null = Add-Type -AssemblyName 'System.IO.Compression.FileSystem'
|
|
|
|
$Null = [System.IO.Compression.ZipFile]::ExtractToDirectory("$($ToolDownloadPath.FullName)", "$($ToolExtractionDirectory.FullName)")
|
|
}
|
|
|
|
{($_ -iin @('.7z'))}
|
|
{
|
|
$Null = $ExpandSevenZipArchive.InvokeReturnAsIs($ToolDownloadPath, $ToolExtractionDirectory)
|
|
}
|
|
|
|
Default
|
|
{
|
|
Throw "The archive extension of `"$($_)`" is not supported. Supported extensions: .zip, .7z"
|
|
}
|
|
}
|
|
}
|
|
|
|
'(^MSI$)'
|
|
{
|
|
$Null = $ExpandMSIPackage.InvokeReturnAsIs($ToolDownloadPath, $ToolExtractionDirectory)
|
|
}
|
|
|
|
'(^DellUpdatePackage$)'
|
|
{
|
|
#region Carve the embedded 7-Zip payload out of the Dell Update Package
|
|
#A Dell Update Package is a portable executable with a 7-Zip archive embedded within it. The archive begins at the first occurrence of the 7-Zip binary signature (37 7A BC AF 27 1C) that is followed by the format version bytes (00 04). Candidate signatures without the version bytes are false positives within the executable code.
|
|
$WriteLogMessage.Invoke(0, @("Attempting to locate the embedded 7-Zip payload within `"$($ToolDownloadPath.FullName)`". Please Wait..."))
|
|
|
|
[System.Byte[]]$ToolPackageByteList = [System.IO.File]::ReadAllBytes($ToolDownloadPath.FullName)
|
|
|
|
[Int64]$SevenZipArchiveSignatureOffset = -1
|
|
|
|
[Int64]$SevenZipArchiveSignatureCandidateIndex = [System.Array]::IndexOf($ToolPackageByteList, [System.Byte]0x37, 0)
|
|
|
|
:SevenZipArchiveSignatureScanLoop While ($SevenZipArchiveSignatureCandidateIndex -gt -1)
|
|
{
|
|
Switch (($SevenZipArchiveSignatureCandidateIndex + 8) -lt $ToolPackageByteList.Length)
|
|
{
|
|
{($_ -eq $True)}
|
|
{
|
|
Switch (($ToolPackageByteList[$SevenZipArchiveSignatureCandidateIndex + 1] -eq 0x7A) -and ($ToolPackageByteList[$SevenZipArchiveSignatureCandidateIndex + 2] -eq 0xBC) -and ($ToolPackageByteList[$SevenZipArchiveSignatureCandidateIndex + 3] -eq 0xAF) -and ($ToolPackageByteList[$SevenZipArchiveSignatureCandidateIndex + 4] -eq 0x27) -and ($ToolPackageByteList[$SevenZipArchiveSignatureCandidateIndex + 5] -eq 0x1C) -and ($ToolPackageByteList[$SevenZipArchiveSignatureCandidateIndex + 6] -eq 0x00) -and ($ToolPackageByteList[$SevenZipArchiveSignatureCandidateIndex + 7] -eq 0x04))
|
|
{
|
|
{($_ -eq $True)}
|
|
{
|
|
[Int64]$SevenZipArchiveSignatureOffset = $SevenZipArchiveSignatureCandidateIndex
|
|
|
|
Break SevenZipArchiveSignatureScanLoop
|
|
}
|
|
}
|
|
}
|
|
|
|
Default
|
|
{
|
|
Break SevenZipArchiveSignatureScanLoop
|
|
}
|
|
}
|
|
|
|
[Int64]$SevenZipArchiveSignatureCandidateIndex = [System.Array]::IndexOf($ToolPackageByteList, [System.Byte]0x37, $SevenZipArchiveSignatureCandidateIndex + 1)
|
|
}
|
|
|
|
Switch ($SevenZipArchiveSignatureOffset -gt -1)
|
|
{
|
|
{($_ -eq $True)}
|
|
{
|
|
$WriteLogMessage.Invoke(0, @("The embedded 7-Zip payload was located at byte offset $($SevenZipArchiveSignatureOffset)."))
|
|
}
|
|
|
|
Default
|
|
{
|
|
Throw "An embedded 7-Zip payload could not be located within `"$($ToolDownloadPath.FullName)`". The downloaded file may not be a Dell Update Package."
|
|
}
|
|
}
|
|
|
|
[System.IO.FileInfo]$ToolPayloadArchivePath = [System.IO.Path]::Combine("$($ToolWorkingDirectory.FullName)", 'Payload.7z')
|
|
|
|
$ToolPayloadArchiveStream = New-Object -TypeName 'System.IO.FileStream' -ArgumentList @("$($ToolPayloadArchivePath.FullName)", [System.IO.FileMode]::Create)
|
|
|
|
Try
|
|
{
|
|
$Null = $ToolPayloadArchiveStream.Write($ToolPackageByteList, $SevenZipArchiveSignatureOffset, $ToolPackageByteList.Length - $SevenZipArchiveSignatureOffset)
|
|
}
|
|
Finally
|
|
{
|
|
$Null = $ToolPayloadArchiveStream.Dispose()
|
|
}
|
|
#endregion
|
|
|
|
#region Extract the carved payload and expand the MSI that it contains
|
|
[System.IO.DirectoryInfo]$ToolPayloadDirectory = [System.IO.Path]::Combine("$($ToolWorkingDirectory.FullName)", 'Payload')
|
|
|
|
$Null = $ExpandSevenZipArchive.InvokeReturnAsIs($ToolPayloadArchivePath, $ToolPayloadDirectory)
|
|
|
|
$ToolPayloadMSIObject = Get-ChildItem -Path ($ToolPayloadDirectory.FullName) -Filter '*.msi' | Select-Object -First 1
|
|
|
|
Switch ($Null -ieq $ToolPayloadMSIObject)
|
|
{
|
|
{($_ -eq $True)}
|
|
{
|
|
Throw "An MSI could not be located within the extracted Dell Update Package payload at `"$($ToolPayloadDirectory.FullName)`"."
|
|
}
|
|
}
|
|
|
|
$Null = $ExpandMSIPackage.InvokeReturnAsIs(([System.IO.FileInfo]$ToolPayloadMSIObject.FullName), $ToolExtractionDirectory)
|
|
#endregion
|
|
}
|
|
|
|
Default
|
|
{
|
|
Throw "The tool type of `"$($ToolDefinition.Type)`" is not supported. Supported types: RawFile, Archive, MSI, DellUpdatePackage"
|
|
}
|
|
}
|
|
#endregion
|
|
|
|
#region Place the extracted content into the tools directory based on the destination mapping table
|
|
ForEach ($DestinationMapping In $ToolDefinition.DestinationMappingTable.GetEnumerator())
|
|
{
|
|
[String]$DestinationMappingSource = "$($DestinationMapping.Key)"
|
|
|
|
[String]$DestinationMappingTarget = [System.IO.Path]::Combine("$($ToolsDirectory.FullName)", "$($DestinationMapping.Value)")
|
|
|
|
#region Resolve the source within the extracted tree
|
|
$DestinationMappingSourceObject = $Null
|
|
|
|
Switch ($True)
|
|
{
|
|
{($DestinationMappingSource -ieq '.')}
|
|
{
|
|
$DestinationMappingSourceObject = Get-Item -Path ($ToolDownloadPath.FullName)
|
|
}
|
|
|
|
{($DestinationMappingSource -ine '.') -and ([System.IO.Directory]::Exists([System.IO.Path]::Combine("$($ToolExtractionDirectory.FullName)", $DestinationMappingSource)) -eq $True)}
|
|
{
|
|
$DestinationMappingSourceObject = Get-Item -Path ([System.IO.Path]::Combine("$($ToolExtractionDirectory.FullName)", $DestinationMappingSource))
|
|
}
|
|
|
|
{($DestinationMappingSource -ine '.') -and ($Null -ieq $DestinationMappingSourceObject)}
|
|
{
|
|
$DestinationMappingSourceObject = Get-ChildItem -Path ($ToolExtractionDirectory.FullName) -Recurse -Filter ($DestinationMappingSource) | Select-Object -First 1
|
|
}
|
|
}
|
|
#endregion
|
|
|
|
Switch ($Null -ine $DestinationMappingSourceObject)
|
|
{
|
|
{($_ -eq $True)}
|
|
{
|
|
$WriteLogMessage.Invoke(0, @("Attempting to place `"$($DestinationMappingSourceObject.FullName)`" at `"$($DestinationMappingTarget)`". Please Wait..."))
|
|
|
|
Switch ($DestinationMappingSourceObject.PSIsContainer)
|
|
{
|
|
{($_ -eq $True)}
|
|
{
|
|
Switch ([System.IO.Directory]::Exists($DestinationMappingTarget))
|
|
{
|
|
{($_ -eq $True)}
|
|
{
|
|
$Null = [System.IO.Directory]::Delete($DestinationMappingTarget, $True)
|
|
}
|
|
}
|
|
|
|
$Null = [System.IO.Directory]::CreateDirectory($DestinationMappingTarget)
|
|
|
|
$Null = Copy-Item -Path "$($DestinationMappingSourceObject.FullName)\*" -Destination ($DestinationMappingTarget) -Recurse -Force
|
|
}
|
|
|
|
Default
|
|
{
|
|
$Null = [System.IO.Directory]::CreateDirectory([System.IO.Path]::GetDirectoryName($DestinationMappingTarget))
|
|
|
|
$Null = Copy-Item -Path ($DestinationMappingSourceObject.FullName) -Destination ($DestinationMappingTarget) -Force
|
|
}
|
|
}
|
|
|
|
$OutputObjectProperties.DestinationPathList.Add($DestinationMappingTarget)
|
|
}
|
|
|
|
Default
|
|
{
|
|
$WriteLogMessage.Invoke(2, @("The destination mapping source of `"$($DestinationMappingSource)`" could not be located within `"$($ToolExtractionDirectory.FullName)`". The mapping will be skipped. This is expected when the source content does not contain that architecture."))
|
|
}
|
|
}
|
|
}
|
|
#endregion
|
|
|
|
#region Remove the tool working directory
|
|
Switch ([System.IO.Directory]::Exists($ToolWorkingDirectory.FullName))
|
|
{
|
|
{($_ -eq $True)}
|
|
{
|
|
$WriteLogMessage.Invoke(0, @("Attempting to remove the tool working directory `"$($ToolWorkingDirectory.FullName)`". Please Wait..."))
|
|
|
|
$Null = Try {[System.IO.Directory]::Delete($ToolWorkingDirectory.FullName, $True)} Catch {$Null}
|
|
}
|
|
}
|
|
#endregion
|
|
|
|
$OutputObjectProperties.Staged = ($OutputObjectProperties.DestinationPathList.Count -gt 0)
|
|
|
|
Switch ($OutputObjectProperties.Staged)
|
|
{
|
|
{($_ -eq $True)}
|
|
{
|
|
$WriteLogMessage.Invoke(0, @("Tool `"$($ToolDefinition.Name)`" was staged successfully. [Destination(s): $($OutputObjectProperties.DestinationPathList -Join '; ')]"))
|
|
}
|
|
|
|
Default
|
|
{
|
|
Throw "None of the destination mappings for tool `"$($ToolDefinition.Name)`" could be staged."
|
|
}
|
|
}
|
|
|
|
$OutputObjectList.Add((New-Object -TypeName 'PSObject' -Property ($OutputObjectProperties)))
|
|
}
|
|
Catch
|
|
{
|
|
[Int]$ToolStagingErrorCount = $ToolStagingErrorCount + 1
|
|
|
|
$WriteLogMessage.Invoke(2, @("Tool `"$($ToolDefinition.Name)`" could not be staged.", "Message: $($_.Exception.Message)"))
|
|
|
|
$OutputObjectList.Add((New-Object -TypeName 'PSObject' -Property ($OutputObjectProperties)))
|
|
|
|
Switch ($ContinueOnError.IsPresent)
|
|
{
|
|
{($_ -eq $False)}
|
|
{
|
|
Throw
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
Write-Output -InputObject ($OutputObjectList.ToArray())
|
|
}
|
|
Catch
|
|
{
|
|
$ExceptionPropertyDictionary = New-Object -TypeName 'System.Collections.Specialized.OrderedDictionary'
|
|
$ExceptionPropertyDictionary.Add('Message', $_.Exception.Message)
|
|
$ExceptionPropertyDictionary.Add('Category', $_.Exception.ErrorRecord.FullyQualifiedErrorID)
|
|
$ExceptionPropertyDictionary.Add('LineNumber', $_.InvocationInfo.ScriptLineNumber)
|
|
$ExceptionPropertyDictionary.Add('LinePosition', $_.InvocationInfo.OffsetInLine)
|
|
$ExceptionPropertyDictionary.Add('Code', $_.InvocationInfo.Line.Trim())
|
|
|
|
$ExceptionMessageList = New-Object -TypeName 'System.Collections.Generic.List[String]'
|
|
|
|
ForEach ($ExceptionProperty In $ExceptionPropertyDictionary.GetEnumerator())
|
|
{
|
|
$ExceptionMessageList.Add("[$($ExceptionProperty.Key): $($ExceptionProperty.Value)]")
|
|
}
|
|
|
|
$WriteLogMessage.Invoke(2, @("$($ExceptionMessageList -Join ' ')"))
|
|
|
|
Throw
|
|
}
|
|
Finally
|
|
{
|
|
$WriteLogMessage.Invoke(0, @("Function `'$($CmdletName)`' is completed."))
|
|
}
|
|
}
|
|
#endregion
|