NanoServerPackage.psm1

#region Script variables

Microsoft.PowerShell.Core\Set-StrictMode -Version Latest

$script:providerName ="NanoServerPackage"
$script:WindowsPackageExtension = ".cab"
$script:onlinePackageCache = @{}
$script:imageCultureCache = @{}
$script:imagePathCache = @{}
$script:wildcardOptions = [System.Management.Automation.WildcardOptions]::CultureInvariant -bor `
                          [System.Management.Automation.WildcardOptions]::IgnoreCase

$script:WindowsPackage = "$env:LOCALAPPDATA\NanoServerPackageProvider"
$script:downloadedCabLocation = "$script:WindowsPackage\DownloadedCabs"
$script:file_modules = "$script:WindowsPackage\sources.txt"
$script:windowsPackageSources = $null
$script:defaultPackageName = "NanoServerPackageSource"
$script:defaultPackageLocation = "http://go.microsoft.com/fwlink/?LinkID=730617&clcid=0x409"
$script:isNanoServerInitialized = $false
$script:isNanoServer = $false
$separator = "|#|"

#endregion Script variables

#region Stand-Alone

function Find-NanoServerPackage
{
    [cmdletbinding()]
    param
    (
        [Parameter(Mandatory=$false,
                        Position=0)]
        [ValidateNotNullOrEmpty()]
        [string[]]
        $Name,

        [System.Version]
        $MinimumVersion,

        [System.Version]
        $MaximumVersion,
        
        [System.Version]
        $RequiredVersion,

        [switch]
        $AllVersions,
        
# [string[]]
# $Repository,

        [string]
        $Culture,

        [switch]
        $Force
    )
    
    Find -Name $Name `
        -MinimumVersion $MinimumVersion `
        -MaximumVersion $MaximumVersion `
        -RequiredVersion $RequiredVersion `
        -AllVersions:$AllVersions `
        -Culture $Culture `
        -Force:$Force
# -Repository $Repository
}

function Save-NanoServerPackage
{
    [CmdletBinding(DefaultParameterSetName='NameAndPathParameterSet',
                   SupportsShouldProcess=$true)]
    Param
    (
        [Parameter(Mandatory=$true, 
                   ValueFromPipelineByPropertyName=$true,
                   Position=0,
                   ParameterSetName='NameAndPathParameterSet')]
        [Parameter(Mandatory=$true, 
                   ValueFromPipelineByPropertyName=$true,
                   Position=0,
                   ParameterSetName='NameAndLiteralPathParameterSet')]
        [ValidateNotNullOrEmpty()]

        [string[]]
        $Name,
        
        [Parameter(Mandatory=$true, 
                   ValueFromPipeline=$true,
                   ValueFromPipelineByPropertyName=$true,
                   ParameterSetName='InputOjectAndPathParameterSet')]
        [Parameter(Mandatory=$true, 
                   ValueFromPipeline=$true,
                   ValueFromPipelineByPropertyName=$true,
                   ParameterSetName='InputOjectAndLiteralPathParameterSet')]
        [ValidateNotNull()]
        [PSCustomObject[]]
        $InputObject,
        
        [Parameter(Mandatory=$false, 
                   ValueFromPipelineByPropertyName=$true,
                   ParameterSetName='NameAndPathParameterSet')]
        [Parameter(Mandatory=$false, 
                   ValueFromPipelineByPropertyName=$true,
                   ParameterSetName='NameAndLiteralPathParameterSet')]
        [string]
        $Culture,

        [Parameter(ValueFromPipelineByPropertyName=$true,
                   ParameterSetName='NameAndPathParameterSet')]
        [Parameter(ValueFromPipelineByPropertyName=$true,
                   ParameterSetName='NameAndLiteralPathParameterSet')]
        [Version]
        $MinimumVersion,

        [Parameter(ValueFromPipelineByPropertyName=$true,
                   ParameterSetName='NameAndPathParameterSet')]
        [Parameter(ValueFromPipelineByPropertyName=$true,
                   ParameterSetName='NameAndLiteralPathParameterSet')]
        [Version]
        $MaximumVersion,
        
        [Parameter(ValueFromPipelineByPropertyName=$true,
                   ParameterSetName='NameAndPathParameterSet')]
        [Parameter(ValueFromPipelineByPropertyName=$true,
                   ParameterSetName='NameAndLiteralPathParameterSet')]
        [Alias('Version')]
        [Version]
        $RequiredVersion,

<#
        [Parameter(ValueFromPipelineByPropertyName=$true,
                   ParameterSetName='NameAndPathParameterSet')]
        [Parameter(ValueFromPipelineByPropertyName=$true,
                   ParameterSetName='NameAndLiteralPathParameterSet')]
        [ValidateNotNullOrEmpty()]
        [string[]]
        $Repository,
#>


        [Parameter(Mandatory=$true, ParameterSetName='NameAndPathParameterSet')]
        [Parameter(Mandatory=$true, ParameterSetName='InputOjectAndPathParameterSet')]
        [string]
        $Path,

        [Parameter(Mandatory=$true, ParameterSetName='NameAndLiteralPathParameterSet')]
        [Parameter(Mandatory=$true, ParameterSetName='InputOjectAndLiteralPathParameterSet')]
        [string]
        $LiteralPath,

        [Parameter()]
        [switch]
        $Force
    )

    Begin
    {
    }

    Process
    {
        # verify name does not have wild card
        foreach ($packageName in $Name)
        {            
            if ([System.Management.Automation.WildcardPattern]::ContainsWildcardCharacters($packageName))
            {
                ThrowError -CallerPSCmdlet $PSCmdlet `
                            -ExceptionName System.Exception `
                            -ExceptionMessage "Name cannot contain wildcards" `
                            -ExceptionObject $packageName `
                            -ErrorId WildCardCharsAreNotSupported `
                            -ErrorCategory InvalidData

                return
            }
        }

        if($InputObject)
        {
            $Name = $InputObject.Name
            $RequiredVersion = $InputObject.Version
            #$Repository = $InputObject.Repository
            $Culture = $InputObject.Culture

            if (-not [string]::IsNullOrWhiteSpace($Culture) -and $Culture.Contains(','))
            {
                $Culture = ''
            }
        }

        if($Path)
        {
            $destinationPath = Resolve-PathHelper -Path $Path `
                                                    -CallerPSCmdlet $PSCmdlet | Microsoft.PowerShell.Utility\Select-Object -First 1

            if(-not $destinationPath -or -not (Microsoft.PowerShell.Management\Test-path $destinationPath))
            {
                $errorMessage = ("Cannot find the path '{0}' because it does not exist" -f $Path)
                ThrowError  -ExceptionName "System.ArgumentException" `
                            -ExceptionMessage $errorMessage `
                            -ErrorId "PathNotFound" `
                            -CallerPSCmdlet $PSCmdlet `
                            -ExceptionObject $Path `
                            -ErrorCategory InvalidArgument
            }
        }
        else
        {
            $destinationPath = Resolve-PathHelper -Path $LiteralPath `
                                                    -IsLiteralPath `
                                                    -CallerPSCmdlet $PSCmdlet | Microsoft.PowerShell.Utility\Select-Object -First 1

            if(-not $destinationPath -or -not (Microsoft.PowerShell.Management\Test-Path -LiteralPath $destinationPath))
            {
                $errorMessage = ("Cannot find the path '{0}' because it does not exist" -f $LiteralPath)
                ThrowError  -ExceptionName "System.ArgumentException" `
                            -ExceptionMessage $errorMessage `
                            -ErrorId "PathNotFound" `
                            -CallerPSCmdlet $PSCmdlet `
                            -ExceptionObject $LiteralPath `
                            -ErrorCategory InvalidArgument
            }
        }

        if($Name)
        {
            # no culture given, use culture of the system
            if ([string]::IsNullOrWhiteSpace($Culture))
            {
                $Culture = (Get-Culture).Name
            }

            $listOfNames = @()
            foreach ($packageName in $Name)
            {
                $listOfNames += $packagename
            }

            $findResults = @()
            $findResults += (Find -Name $listOfNames `
                            -MinimumVersion $MinimumVersion `
                            -MaximumVersion $MaximumVersion `
                            -RequiredVersion $RequiredVersion `
                            -Culture $Culture `
                            -Force:$Force)
# -Repository $Repository `

            if ($findResults.Count -eq 0)
            {
                Write-Error "No results found for $listOfNames"
                return
            }

            foreach($result in $findResults)
            {
                $currLang = $result.Culture

                # Base Installer
                $fileName_base = Get-FileName -name $result.Name `
                                                -Culture "" `
                                                -version $result.Version.ToString()

                $destination_base = Join-Path $destinationPath $fileName_base

                if($PSCmdlet.ShouldProcess($fileName_base, "Save-NanoServerPackage"))
                {
                    if(Test-Path $destination_base)
                    {
                        if($Force)
                        {
                            Remove-Item $destination_base

                            $token = $result.Locations.base
                            DownloadFile -downloadURL $token -destination $destination_base
                        }
                        else
                        {
                            # The file exists, not downloading
                            Write-Information "$fileName_base already existsat $destinationPath. Skipping save."
                        }
                    }
                    else
                    {
                        $token = $result.Locations.base
                        DownloadFile -downloadURL $token -destination $destination_base
                    }
                }

                # Language Installer
                $fileName_lang = Get-FileName -name $result.Name `
                                                -Culture $currLang `
                                                -version $result.Version.ToString()

                $destination_lang = Join-Path $destinationPath $fileName_lang

                if($PSCmdlet.ShouldProcess($fileName_lang, "Save-NanoServerPackage"))
                {
                    if(Test-Path $destination_lang)
                    {
                        if($Force)
                        {
                            Remove-Item $destination_lang

                            $token = $result.Locations.$currLang
                            DownloadFile -downloadURL $token -destination $destination_lang
                        }
                        else
                        {
                            # The file exists, not downloading
                            Write-Information "$fileName_lang already exists at $destinationPath. Skipping save."
                        }
                    }
                    else
                    {
                        $token = $result.Locations.$currLang
                        DownloadFile -downloadURL $token -destination $destination_lang
                    }
                }

                $result
            }
            
        }
    }

    End
    {
    }
}

function Install-NanoServerPackage
{
    [CmdletBinding(SupportsShouldProcess=$true)]
    param
    (
        [parameter(Mandatory=$true,
            Position=0,
            ValueFromPipelineByPropertyName=$true,
            ParameterSetName='NameParameterSet')]
        [ValidateNotNullOrEmpty()]
        [System.String[]]$Name,

        <#
        [Parameter(Mandatory=$true,
                   ValueFromPipeline=$true,
                   ValueFromPipelineByPropertyName=$true,
                   Position=0,
                   ParameterSetName='InputObject')]
        [ValidateNotNull()]
        [PSCustomObject[]]
        $InputObject
        #>


        [Parameter(ValueFromPipelineByPropertyName=$true,
                   ParameterSetName='NameParameterSet')]
        [ValidateNotNull()]
        [Version]
        $MinimumVersion,

        [parameter(Mandatory=$false,
            ValueFromPipelineByPropertyName=$true,
            ParameterSetName='NameParameterSet')]
        [ValidateNotNullOrEmpty()]
        [System.String]$Culture,

        [Parameter(ValueFromPipelineByPropertyName=$true,
                   ParameterSetName='NameParameterSet')]
        [Alias('Version')]
        [System.Version]$RequiredVersion,

        [Parameter(ValueFromPipelineByPropertyName=$true,
                   ParameterSetName='NameParameterSet')]
        [ValidateNotNull()]
        [System.Version]$MaximumVersion,

        [ValidateNotNullOrEmpty()]
        [System.String]$ToVhd,

        [parameter()]
        [switch]$Force,

        [parameter()]
        [switch]$NoRestart

<# [Parameter(ParameterSetName='NameParameterSet')]
        [ValidateNotNullOrEmpty()]
        [string[]]
        $Repository
#>

    )

    Begin
    {
    }

    Process
    {
        # verify name does not have wild card
        foreach ($packageName in $Name)
        {
            if ([System.Management.Automation.WildcardPattern]::ContainsWildcardCharacters($packageName))
            {
                ThrowError -CallerPSCmdlet $PSCmdlet `
                            -ExceptionName System.Exception `
                            -ExceptionMessage "Name cannot contain wildcards" `
                            -ExceptionObject $packageName `
                            -ErrorId WildCardCharsAreNotSupported `
                            -ErrorCategory InvalidData

                return
            }   
        }

        # pipeline case where culture passed in is en-us, de-de, etc.
        if (-not [string]::IsNullOrWhiteSpace($Culture) -and $Culture.Contains(','))
        {
            $Culture = ''
        }

        $packagesToBeInstalled = @()

        # do a find first, if there are any errors, don't install
        $packagesToBeInstalled += (Find-NanoServerPackage -Name $Name -MinimumVersion $MinimumVersion -MaximumVersion $MaximumVersion -RequiredVersion $RequiredVersion `
            -Culture $Culture -ErrorAction Stop) # -Repository $Repository

        if ($packagesToBeInstalled.Count -eq 0)
        {
            return
        }

        $mountDrive = $null

        # the available packages on the system
        $availablePackages = $()

        $installedPackage = $null

        if (-not [string]::IsNullOrWhiteSpace($ToVhd))
        {
            $ToVhd = Resolve-PathHelper $ToVhd -callerPSCmdlet $PSCmdlet
            
            if (-not ([System.IO.File]::Exists($ToVhd)))
            {
                $exception = New-Object System.ArgumentException "$ToVhd does not exist"
                $errorCategory = [System.Management.Automation.ErrorCategory]::InvalidArgument
                $errorRecord = New-Object System.Management.Automation.ErrorRecord $exception, "InvalidVhdPath", $errorCategory, $ToVhd

                $PSCmdlet.ThrowTerminatingError($errorRecord)
            }

            # mount image
            $mountDrive = New-MountDrive

            Write-Verbose "Mounting $ToVhd to $mountDrive"

            Write-Progress -Activity "Mounting $ToVhd to $mountDrive" -PercentComplete 0

            $null = Mount-WindowsImage -ImagePath $ToVhd -Index 1 -Path $mountDrive
        }

        $discard = $false

        try
        {
            # If no force, then just check whether the packages are already installed before proceeding
            if (-not $Force)
            {
                Write-Verbose "Getting available packages"

                # installing online
                if ([string]::IsNullOrWhiteSpace($ToVhd))
                {
                    $availablePackages = (Get-WindowsPackage -Online).PackageName.ToLower()
                }
                else
                {
                    Write-Progress -Activity "Getting available packages on $mountDrive" -PercentComplete 10

                    $availablePackages = (Get-WindowsPackage -Path $mountDrive).PackageName.ToLower()
                }
            }

            if($PSCmdlet.ShouldProcess($Name, "Install-NanoServerPackage"))
            {
                [bool]$success = $false

                if (-not [string]::IsNullOrWhiteSpace($ToVhd))
                {
                    Write-Progress -Activity "Mounting $ToVhd to $mountDrive" -PercentComplete 20
                }

                #Installing the package
                $installedPackage = Install-PackageHelper -Name $Name `
                                                            -Culture $Culture `
                                                            -RequiredVersion $RequiredVersion `
                                                            -MinimumVersion $MinimumVersion `
                                                            -MaximumVersion $MaximumVersion `
                                                            -imagePath $ToVhd `
                                                            -mountDrive $mountDrive `
                                                            -availablePackages $availablePackages `
                                                            -successfullyInstalled ([ref]$success) `
                                                            -Force:$Force `
                                                            -NoRestart:$NoRestart `
                                                            -PackagesToBeInstalled $packagesToBeInstalled
#-source $source `

                if (-not $success)
                {
                    $exception = New-Object System.ArgumentException "Cannot install package $packageName with culture $Culture and version $RequiredVersion"
                    $errorCategory = [System.Management.Automation.ErrorCategory]::InvalidArgument
                    $errorRecord = New-Object System.Management.Automation.ErrorRecord $exception, "FailedToInstallPackage", $errorCategory, $packageName

                    Write-Error $errorRecord
                    $discard = $true
                    break
                }

                $installedPackage
            }
        }
        catch
        {
            $errorCategory = [System.Management.Automation.ErrorCategory]::InvalidArgument
            $errorRecord = New-Object System.Management.Automation.ErrorRecord $_.Exception, "FailedToInstallPackage", $errorCategory, $Name
            Write-Error $errorRecord
            $discard = $true
        }
        finally
        {
            # unmount the drive
            if ($null -ne $mountDrive)
            {
                Write-Progress -Activity "Unmounting mount drive $mountDrive" -PercentComplete 90
                Write-Verbose "Unmounting mount drive $mountDrive"
                Remove-MountDrive $mountDrive -discard $discard
                Write-Progress -Completed -Activity "Completed"
            }
        }
    }

    End
    {
    }
}

#endregion Stand-Alone

#region Helpers

function Find
{
    [CmdletBinding()]
    param
    (
        [string[]]
        $Name,

        [System.Version]
        $MinimumVersion,

        [System.Version]
        $MaximumVersion,
        
        [System.Version]
        $RequiredVersion,

        [switch]
        $AllVersions,

        <#[string[]]
        $Repository,
        #>


        [string]
        $Culture,

        [switch]
        $Force
    )

    if(-not (CheckVersion $MinimumVersion $MaximumVersion $RequiredVersion $AllVersions))
    {            
        return $null
    }

    $allSources = Get-Source #$Repository

    $searchResults = @()

    if ($null -eq $Name -or $Name.Count -eq 0)
    {
        $Name = @('')
    }

    foreach($currSource in $allSources)
    {
        foreach ($singleName in $Name)
        {
            if ([string]::IsNullOrWhiteSpace($singleName) -or $singleName.Trim() -eq '*')
            {
                # if no name is supplied but min or max version is supplied, error out
                if ($null -ne $MinimumVersion -or $null -ne $MaximumVersion)
                {
                    ThrowError -CallerPSCmdlet $PSCmdlet `
                                -ExceptionName System.Exception `
                                -ExceptionMessage "Name is required when either MinimumVersion or MaximumVersion parameter is used" `
                                -ExceptionObject $Name `
                                -ErrorId NameRequiredForMinOrMaxVersion `
                                -ErrorCategory InvalidData
                }
            }

            $result = Find-Azure -Name $singleName `
                                    -MinimumVersion $MinimumVersion `
                                    -MaximumVersion $MaximumVersion `
                                    -RequiredVersion $RequiredVersion `
                                    -AllVersions:$AllVersions `
                                    -Repository $currSource `
                                    -Culture $Culture `
                                    -Force:$Force
            
            if($null -eq $result)
            {
                # Error must have been thrown already
                # Just continue
                continue
            }
            
            if ($result.GetType().IsArray -and $result.Count -eq 0)
            {
                $sourceName = $currSource.Name
                Write-Error "No matching packages could be found for $singleName in $sourceName"
                continue
            }

            $searchResults += $result
        }
    }

    return $searchResults
}

function Find-Azure
{
    param
    (
        [string]
        $Name,

        [System.Version]
        $MinimumVersion,

        [System.Version]
        $MaximumVersion,
        
        [System.Version]
        $RequiredVersion,

        [switch]
        $AllVersions,

        [System.Object]
        $Repository,

        [string]
        $Culture,

        [switch]
        $Force
    )

    $searchFile = Get-SearchIndex -Force:$Force -fwdLink $Repository.SourceLocation
    $searchFileContent = Get-Content $searchFile

    if($null -eq $searchFileContent)
    {
        return $null
    }

    if(IsNanoServer)
    {
        $jsonDll = [Microsoft.PowerShell.CoreCLR.AssemblyExtensions]::LoadFrom($PSScriptRoot + "\Json.coreclr.dll")
        $jsonParser = $jsonDll.GetTypes() | Where-Object name -match jsonparser
        $searchContent = $jsonParser::FromJson($searchFileContent)
        $searchStuff = $searchContent.Get_Item("array0")
        $searchData = @()
        foreach($searchStuffEntry in $searchStuff)
        {
            $obj = New-Object PSObject 
            $obj | Add-Member NoteProperty Name $searchStuffEntry.Name
            $obj | Add-Member NoteProperty Version $searchStuffEntry.Version
            $obj | Add-Member NoteProperty Description $searchStuffEntry.Description
            
            $languageObj = New-Object PSObject
            $languageDictionary = $searchStuffEntry.Language
            $languageDictionary.Keys | ForEach-Object {
                $languageObj | Add-Member NoteProperty $_ $languageDictionary.Item($_)
            }

            $obj | Add-Member NoteProperty Language $languageObj
            $searchData += $obj
        }
    }
    else
    {
        $searchData = $searchFileContent | ConvertFrom-Json
    }

    $searchResults = @()
    $searchDictionary = @{}

    # If name is null or whitespace, interpret as *
    if ([string]::IsNullOrWhiteSpace($Name))
    {
        $Name = "*"
    }

    # Handle the version not given scenario
    if((-not ($MinimumVersion -or $MaximumVersion -or $RequiredVersion -or $AllVersions)))
    {
        $MinimumVersion = [System.Version]'0.0.0.0'
    }

    foreach($entry in $searchData)
    {
        $toggle = $false

        # Check if the search string has * in it
        if ([System.Management.Automation.WildcardPattern]::ContainsWildcardCharacters($Name))
        {
            if($entry.name -like $Name)
            {
                $toggle = $true
            }
            else
            {
                continue
            }
        }
        else
        {
            if($entry.name -eq $Name)
            {
                $toggle = $true
            }
            else
            {
                continue
            }
        }

        $thisVersion = Convert-Version $entry.version

        if($MinimumVersion)
        {
            $convertedMinimumVersion = Convert-Version $MinimumVersion

            if(($thisVersion -ge $convertedMinimumVersion))
            {
                if($searchDictionary.ContainsKey($entry.name))
                {
                    $objEntry = $searchDictionary[$entry.name]
                    $objVersion = Convert-Version $objEntry.Version

                    if($thisVersion -gt $objVersion)
                    {
                        $toggle = $true
                    }
                    else
                    {
                        $toggle = $false
                    }
                }
                else
                {
                    $toggle = $true
                }   
            }
            else
            {
                $toggle = $false
            }
        }

        if($MaximumVersion)
        {
            $convertedMaximumVersion = Convert-Version $MaximumVersion

            if(($thisVersion -le $convertedMaximumVersion))
            {
                if($searchDictionary.ContainsKey($entry.name))
                {
                    $objEntry = $searchDictionary[$entry.name]
                    $objVersion = Convert-Version $objEntry.Version

                    if($thisVersion -gt $objVersion)
                    {
                        $toggle = $true
                    }
                    else
                    {
                        $toggle = $false
                    }
                }
                else
                {
                    $toggle = $true
                }
            }
            else
            {
                $toggle = $false
            }
        }

        if($RequiredVersion)
        {
            $convertedRequiredVersion = Convert-Version $RequiredVersion

            if(($thisVersion -eq $convertedRequiredVersion))
            {
                $toggle = $true                
            }
            else
            {
                $toggle = $false
            }
        }

        if($AllVersions)
        {
            if($toggle)
            {
                $searchResults += $entry
            }
        }

        if($toggle)
        {
            if($searchDictionary.ContainsKey($entry.name))
            {
                $searchDictionary.Remove($entry.Name)
            }
            
            $searchDictionary.Add($entry.name, $entry)
        }
    }

    if(-not $AllVersions)
    {
        $searchDictionary.Keys | ForEach-Object {
                $searchResults += $searchDictionary.Item($_)
            }
    }

    $searchLanguageResults = @()

    foreach($searchEntry in $searchResults)
    {
        $EntryName = $searchEntry.Name
        $EntryVersion = $searchEntry.Version
        $EntryDescription = $searchEntry.Description
        $langDict = $searchEntry.Language
        $props= Get-Member -InputObject $langDict -MemberType NoteProperty
        $theSource = $Repository.Name

        if (-not [string]::IsNullOrWhiteSpace($Culture))
        {
            if(($props.Name -notcontains $Culture) -or `
                ($Culture -eq "base"))
            {
                ThrowError -CallerPSCmdlet $PSCmdlet `
                            -ExceptionName System.Exception `
                            -ExceptionMessage "Culture: $Culture is not supported" `
                            -ExceptionObject $EntryName `
                            -ErrorId WildCardCharsAreNotSupported `
                            -ErrorCategory InvalidData
                return
            }

            $languageObj = New-Object PSObject
            $languageObj | Add-Member NoteProperty "base" $langDict."base"
            $languageObj | Add-Member NoteProperty $Culture $langDict.$Culture

            $ResultEntry = Microsoft.PowerShell.Utility\New-Object PSCustomObject -Property ([ordered]@{
                Name = $EntryName
                Version = $EntryVersion
                Description = $EntryDescription
                Source = $theSource
                Locations = $languageObj
                Culture = $Culture
            })
            $ResultEntry.PSTypeNames.Insert(0, "Microsoft.PowerShell.Commands.NanoServerPackageItemInfo")
            $searchLanguageResults += $ResultEntry
        }
        else
        {
            $langList = @()
            $langListString = ""

            $props.Name | ForEach-Object {
                $langList += $_
                if($_ -ne "base"){
                    $langListString += $_
                    $langListString += ", "
                }
            }

            $langListString = $langListString.Substring(0, $langListString.Length - 2)

            $ResultEntry = Microsoft.PowerShell.Utility\New-Object PSCustomObject -Property ([ordered]@{
                Name = $EntryName
                Version = $EntryVersion
                Description = $EntryDescription
                Source = $theSource
                Locations = $langDict
                Culture = $langListString
            })
            $ResultEntry.PSTypeNames.Insert(0, "Microsoft.PowerShell.Commands.NanoServerPackageItemInfo")
            $searchLanguageResults += $ResultEntry
        }
    }
    
    return $searchLanguageResults
}

###
### SUMMARY: Download the file given the URI to the given location
###
function DownloadFile
{
    [CmdletBinding()]
    param($downloadURL, $destination)
    
    $startTime = Get-Date

    try
    {
        # Download the file
    Write-Verbose "Downloading $downloadUrl to $destination"
    $saveItemPath = $PSScriptRoot + "\SaveHTTPItemUsingBITS.psm1"
    Import-Module "$saveItemPath"
    Save-HTTPItemUsingBitsTransfer -Uri $downloadURL `
                    -Destination $destination
    Write-Verbose "Finished downloading"

        $endTime = Get-Date
        $difference = New-TimeSpan -Start $startTime -End $endTime
        $downloadTime = "Downloaded in " + $difference.Hours + " hours, " + $difference.Minutes + " minutes, " + $difference.Seconds + " seconds."
        Write-Verbose $downloadTime
    }
    catch
    {
        ThrowError -CallerPSCmdlet $PSCmdlet `
                    -ExceptionName $_.Exception.GetType().FullName `
                    -ExceptionMessage $_.Exception.Message `
                    -ExceptionObject $downloadURL `
                    -ErrorId FailedToDownload `
                    -ErrorCategory InvalidOperation        
    }
}

function Install-PackageHelper
{
    [cmdletbinding()]
    param(
        [string[]]$Name,
        [string]$Culture,
        [string]$source,
        [string]$mountDrive,
        [string]$imagePath,
        [ref]$successfullyInstalled,
        [version]$MinimumVersion,
        [version]$MaximumVersion,
        [version][Alias('Version')]$RequiredVersion,
        [string[]]$availablePackages,
        [switch]$Force,
        [switch]$NoRestart,
        [PSCustomObject[]]$PackagesToBeInstalled
    )

    $installedWindowsPackages = @()

    $successfullyInstalled.Value = $false

    if ([string]::IsNullOrWhiteSpace($Culture))
    {
        # if the culture is null for the online case, we can find out easily

        if ([string]::IsNullOrWhiteSpace($mountDrive))
        {
            $Culture = (Get-Culture).Name
        }
        else
        {
            Write-Verbose "Determining the culture of $mountDrive"

            $fileKey = Get-FileKey -filePath $imagePath

            if (-not $script:imageCultureCache.ContainsKey($fileKey))
            {
                $Culture = Get-ImageCulture -mountDrive $mountDrive

                if ($null -eq $Culture)
                {
                    Write-Verbose "Cannot determine culture of $mountDrive with /Get-Intl. Trying to find culture using a sample package"

                    $packagesOnTheMachine = $availablePackages

                    if ($null -eq $packagesOnTheMachine -or $packagesOnTheMachine.Count -eq 0)
                    {
                        $packagesOnTheMachine = (Get-WindowsPackage -Path $mountDrive).PackageName
                    }

                    foreach ($package in $packagesOnTheMachine)
                    {
                        $Culture = $package.Split('~')[3]

                        # we have found a culture from a package installed!
                        if (-not [string]::IsNullOrWhiteSpace($Culture))
                        {
                            break
                        }
                    }
                }

                # if after all that, culture still null then we have to abort
                if ($null -eq $Culture)
                {
                    Write-Warning "Cannot determine culture of the vhd. Please supply it directly."
                    return
                }

                $script:imageCultureCache[$fileKey] = $Culture
            }
            else
            {
                $Culture = $script:imageCultureCache[$fileKey]
            }
        }

        Write-Verbose "The culture to be installed is $Culture"
    }

    foreach ($packageName in $Name)
    { 
        $randomName = [System.IO.Path]::GetRandomFileName()
        $destinationFolder = Join-Path $script:downloadedCabLocation $randomName

        $baseVersion = $null
        $languageVersion = $null

        foreach ($availablePackage in $availablePackages)
        {
            # check whether base package is already installed
            if (Test-PackageWithSearchQuery -fullyQualifiedName $availablePackage -name $packageName -requiredVersion $RequiredVersion -minimumVersion $MinimumVersion -maximumVersion $MaximumVersion -Culture "Base")
            {
                $baseVersion = Convert-Version ($availablePackage.Split('~')[4])
            }
            # check whether language pack is installed
            elseif (Test-PackageWithSearchQuery -fullyQualifiedName $availablePackage -name $packageName -requiredVersion $RequiredVersion -minimumVersion $MinimumVersion -maximumVersion $MaximumVersion -Culture $Culture)
           {
                $languageVersion = Convert-Version ($availablePackage.Split('~')[4])
            }
        }

        # no force and both are installed, just returned
        if (-not $Force)
        {
            if ($null -ne $baseVersion -and $null -ne $languageVersion)
            {
                Write-Verbose "Skipping installed package $packageName"
                $successfullyInstalled.Value = $true

                # returned the package to be installed

                if ($null -ne $PackagesToBeInstalled)
                {
                    $PackagesToBeInstalled | Where-Object {$_.Name -eq $packageName} | ForEach-Object {$_.Culture = $Culture; $_}
                }

                continue
            }
        }

        # This means source is offline
        if ((-not [string]::IsNullOrWhiteSpace($source)) -and (Test-Path $source))
        {
            Write-Verbose "Installing package from $source"
            $savedCabFilesToInstall = @($source)
        }
        else
        {
            if (-not (Test-Path $destinationFolder))
            {
                $null = mkdir $destinationFolder
            }

            Write-Verbose "Downloading cab files to $destinationFolder"
            $savedPackages = Save-NanoServerPackage -Name $packageName -Culture $Culture -RequiredVersion $RequiredVersion -MinimumVersion $MinimumVersion `
                                                -MaximumVersion $MaximumVersion -Path $destinationFolder -Force
        }

        $savedCabFilesToInstall = @()

        foreach ($savedPackage in $savedPackages)
        {
            # we only proceed to install to base if -force is true or if base is not already installed
            # the base version needs to match the package version, otherwise we have to download the newer base version (so it matches the language version)
            if ($Force -or (-not $baseVersion -eq $savedPackage.Version))
            {
                $savedCabFilesToInstall += (Join-Path $destinationFolder (Get-FileName -name $savedPackage.Name -Culture "" -version $savedPackage.Version))
            }

            # we only proceed to install to language if -force is true or if language is not already installed
            if ($Force -or (-not $languageVersion -eq $savedPackage.Version))
            {
                $savedCabFilesToInstall += (Join-Path $destinationFolder (Get-FileName -name $savedPackage.Name -Culture $Culture -version $savedPackage.Version))

                $installedWindowsPackages += $savedPackage
            }
        }

        $restartNeeded = $false

        try
        {
            # Installing offline scenario
            if (-not [string]::IsNullOrWhiteSpace($mountDrive))
            {
                # in this scenario, the function that calls us already mount the drive
                Write-Verbose "Installing to mountdrive $mountDrive"
                $successfullyInstalled.Value = Install-CabOfflineFromPath -mountDrive $mountDrive -packagePaths $savedCabFilesToInstall
            }
            else
            {
                Write-Verbose "Installing cab files $savedCabFilesToInstall"                
                $successfullyInstalled.Value = Install-Online $savedCabFilesToInstall -restartNeeded ([ref]$restartNeeded)

                if ($restartNeeded -and (-not $NoRestart))
                {
                    Write-Warning "Restart is needed to complete installation"
                }
            }
        
        }
        catch
        {
            $successfullyInstalled.Value = $false
            ThrowError -CallerPSCmdlet $PSCmdlet `
                        -ExceptionName $_.Exception.GetType().FullName `
                        -ExceptionMessage $_.Exception.Message `
                        -ExceptionObject $Name `
                        -ErrorId FailedToInstall `
                        -ErrorCategory InvalidOperation            
        }
        finally
        {
            # Remove the online source
            if (([string]::IsNullOrWhiteSpace($source)) -or (-not (Test-Path $source)))
            {
                Remove-Item $destinationFolder -Recurse -Force
            }
        }
    }

    $installedWindowsPackages
}

###
### SUMMARY: Checks if the system is nano server or not
### Look into the win32 operating system class
### Returns True if running on Nano
### False otherwise
###
function IsNanoServer
{
    if ($script:isNanoServerInitialized)
    {
        return $script:isNanoServer
    }
    else
    {
        $script:isNanoServerInitialized = $true
        $operatingSystem = Get-CimInstance -ClassName win32_operatingsystem
        $systemSKU = $operatingSystem.OperatingSystemSKU
        $script:isNanoServer = ($systemSKU -eq 109) -or ($systemSKU -eq 144) -or ($systemSKU -eq 143)
        return $script:isNanoServer
    }
}

###
### SUMMARY: Checks if the given destination is kosher or not
###
function CheckDestination
{
    param($Destination)

    # Check if entire path is folder structure
    $dest_item = Get-Item $Destination `
                            -ErrorAction SilentlyContinue `
                            -WarningAction SilentlyContinue

    if($dest_item -is [System.IO.DirectoryInfo])
    {
        return $true
    }
    else
    {
        Write-Verbose "Creating directory structure: $Destination"
        mkdir $Destination
        return $true
    }

    return $false
}         

function CheckVersion
{
    param
    (
        [System.Version]$MinimumVersion,
        [System.Version]$MaximumVersion,
        [System.Version]$RequiredVersion,
        [switch]$AllVersions
    )

    if($AllVersions -and $RequiredVersion)
    {
        Write-Error "AllVersions and RequiredVersion cannot be used together"
        return $false
    }

    if($AllVersions -or $RequiredVersion)
    {
        if($MinimumVersion -or $MaximumVersion)
        {
            Write-Error "AllVersions and RequiredVersion switch cannot be used with MinimumVersion or MaximumVersion"
            return $false
        }
    }

    if($MinimumVersion -and $MaximumVersion)
    {
        if($MaximumVersion -lt $MinimumVersion)
        {
            Write-Error "Minimum Version cannot be more than Maximum Version"
            return $false
        }
    }

    return $true
}

function Get-FileName
{
    param(
        [string]$Culture,
        [string]$name,
        [string]$version
    )

    $fileName = $name + "_" + $Culture + "_" + $version.replace('.','-') + $script:WindowsPackageExtension
    return $fileName
}

###
### SUMMARY: Get the search index from Azure
###
function Get-SearchIndex
{
    param
    (
        [switch]
        $Force,

        [string]
        $fwdLink
    )
    
    $fullUrl = Resolve-FwdLink $fwdLink
    $fullUrl = $fullUrl.AbsoluteUri
    $destination = $script:WindowsPackage + "\searchNanoPackageIndex.txt"

    if(Test-Path $destination)
    {
        Remove-Item $destination
        DownloadFile -downloadURL $fullUrl `
                -destination $destination
    }
    else
    {
        DownloadFile -downloadURL $fullUrl `
                    -destination $destination
    }
    
    return $destination
} 

function Get-ImageCulture
{
    param
    (
        [string]$mountDrive
    )

    $languageSearch = dism /Image:$mountDrive /Get-Intl

    foreach ($languageString in $languageSearch)
    {
        if ($languageString -match "\s*Default\s*system\s*UI\s*language\s*:\s*([a-z][a-z]-[A-Z][A-Z])\s*")
        {
            return $matches[1]
        }
    }

}

###
### SUMMARY: Resolve the fwdlink to get the actual search URL
###
function Resolve-FwdLink
{
    param
    (
        [parameter(Mandatory=$false)]
        [System.String]$Uri
    )
    
    if(-not (IsNanoServer))
    {
        Add-Type -AssemblyName System.Net.Http
    }
    $httpClient = New-Object System.Net.Http.HttpClient
    $response = $httpclient.GetAsync($Uri)
    $link = $response.Result.RequestMessage.RequestUri


    return $link
}

function Resolve-PathHelper
{
    param 
    (
        [Parameter()]
        [ValidateNotNullOrEmpty()]
        [string[]]
        $path,

        [Parameter()]
        [switch]
        $isLiteralPath,

        [Parameter()]
        [ValidateNotNullOrEmpty()]
        [System.Management.Automation.PSCmdlet]
        $callerPSCmdlet
    )
    
    $resolvedPaths =@()

    foreach($currentPath in $path)
    {
        try
        {
            if($isLiteralPath)
            {
                $currentResolvedPaths = Microsoft.PowerShell.Management\Resolve-Path -LiteralPath $currentPath -ErrorAction Stop
            }
            else
            {
                $currentResolvedPaths = Microsoft.PowerShell.Management\Resolve-Path -Path $currentPath -ErrorAction Stop
            }
        }
        catch
        {
            $errorMessage = ("Cannot find the path '{0}' because it does not exist" -f $currentPath)
            ThrowError  -ExceptionName "System.InvalidOperationException" `
                        -ExceptionMessage $errorMessage `
                        -ErrorId "PathNotFound" `
                        -CallerPSCmdlet $callerPSCmdlet `
                        -ErrorCategory InvalidOperation
        }

        foreach($currentResolvedPath in $currentResolvedPaths)
        {
            $resolvedPaths += $currentResolvedPath.ProviderPath
        }
    }

    $resolvedPaths
}

#endregion Helpers

#region Source

###
### SUMMARY: Gets the source from where to get the images
### Initializes the variables for find, download and install
### RETURN:
### Returns the type of
###
function Get-Source
{
    param($sources)

    Set-ModuleSourcesVariable

    $listOfSources = @()

    # if sources is supplied and we cannot find it, error out
    if((-not [string]::IsNullOrWhiteSpace($sources)) -and (-not $script:windowsPackageSources.Contains($sources)))
    {
        ThrowError -CallerPSCmdlet $PSCmdlet `
                            -ExceptionName System.Exception `
                            -ExceptionMessage "Unable to find package source '$sources'. Use Get-PackageSource to see all available package sources." `
                            -ExceptionObject $sources `
                            -ErrorId WildCardCharsAreNotSupported `
                            -ErrorCategory InvalidData
    }

    foreach($mySource in $script:WindowsPackageSources.Values)
    {
        if((-not $sources) -or
            (($mySource.Name -eq $sources) -or
               ($mySource.Location -eq $sources)))
       {
            $tempHolder = @{}

            $location = $mySource.SourceLocation
            $tempHolder.Add("SourceLocation", $location)
            
            $packageSourceName = $mySource.Name
            $tempHolder.Add("Name", $packageSourceName)
            
            $listOfSources += $tempHolder
        }
    }

    return $listOfSources
}

function Set-ModuleSourcesVariable
{
    if(Microsoft.PowerShell.Management\Test-Path $script:file_modules)
    {
        $script:windowsPackageSources = DeSerializePSObject -Path $script:file_modules
    }
    
    if((-not (Microsoft.PowerShell.Management\Test-Path $script:file_modules)))
    {
        $script:windowsPackageSources = [ordered]@{}
        $defaultModuleName = "NanoServerPackageSource"

        $defaultModuleSource = Microsoft.PowerShell.Utility\New-Object PSCustomObject -Property ([ordered]@{
            Name = $script:defaultPackageName
            SourceLocation = $script:defaultPackageLocation
            Trusted=$false
            Registered= $true
            InstallationPolicy = "Untrusted"
        })

        $script:windowsPackageSources.Add($defaultModuleName, $defaultModuleSource)
        Save-ModuleSource
    }
}

function Get-PackageProviderName
{
    return $script:providerName
}

###
### SUMMARY: Deserializes the PSObject
###
function DeSerializePSObject
{
    [CmdletBinding(PositionalBinding=$false)]    
    Param
    (
        [Parameter(Mandatory=$true)]        
        $Path
    )
    $filecontent = Microsoft.PowerShell.Management\Get-Content -Path $Path
    [System.Management.Automation.PSSerializer]::Deserialize($filecontent)    
}

function Save-ModuleSource
{
    # check if exists
    if(-not (Test-Path $script:WindowsPackage))
    {
        $null = mkdir $script:WindowsPackage
   }

    # seralize module
    Microsoft.PowerShell.Utility\Out-File -FilePath $script:file_modules `
                                            -Force `
                                            -InputObject ([System.Management.Automation.PSSerializer]::Serialize($script:windowsPackageSources))
}

function Resolve-PackageSource
{
    Set-ModuleSourcesVariable

    $SourceName = $request.PackageSources

    if(-not $SourceName)
    {
        $SourceName = "*"
    }

    foreach($moduleSourceName in $SourceName)
    {
        if($request.IsCanceled)
        {
            return
        }
        
        $wildcardPattern = New-Object System.Management.Automation.WildcardPattern $moduleSourceName,$script:wildcardOptions
        $moduleSourceFound = $false
        
        $script:windowsPackageSources.GetEnumerator() | 
            Microsoft.PowerShell.Core\Where-Object {$wildcardPattern.IsMatch($_.Key)} | 
                Microsoft.PowerShell.Core\ForEach-Object {
                    $moduleSource = $script:windowsPackageSources[$_.Key]
                    $packageSource = New-PackageSourceFromModuleSource -ModuleSource $moduleSource
                    Write-Output -InputObject $packageSource
                    $moduleSourceFound = $true
                }

        if(-not $moduleSourceFound)
        {
            $sourceName  = Get-SourceName -Location $moduleSourceName
            if($sourceName)
            {
                $moduleSource = $script:windowsPackageSources[$sourceName]
                $packageSource = New-PackageSourceFromModuleSource -ModuleSource $moduleSource
                Write-Output -InputObject $packageSource
            }
        }
    }
}

function Add-PackageSource
{
    [CmdletBinding()]
    param
    (
        [string]
        $Name,
         
        [string]
        $Location,

        [bool]
        $Trusted
    )

    Set-ModuleSourcesVariable
    
    $options = $request.Options
    $Default = $false

    if($options)
    {
        foreach( $o in $options.Keys )
        {
            Write-Debug ("OPTION dictionary: {0} => {1}" -f ($o, $options[$o]) )
        }

        if($options.ContainsKey('Default'))
        {
            $Default = $options['Default']
        }
    }

    if($Default)
    {
        $Name = $script:defaultPackageName
        $Location = $script:defaultPackageLocation
    }

    # Check if this package source already exists
    foreach($psModuleSource in $script:windowsPackageSources.Values)
    {
        if(($Name -eq $psModuleSource.Name) -or
                ($Location -eq $psModuleSource.SourceLocation))
        {
            throw "Package Source $Name with $Location already exists"
        }
    }

    # Add new module source
    $moduleSource = Microsoft.PowerShell.Utility\New-Object PSCustomObject -Property ([ordered]@{
            Name = $Name
            SourceLocation = $Location            
            Trusted=$Trusted
            Registered= $true
            InstallationPolicy = if($Trusted) {'Trusted'} else {'Untrusted'}
        })

    $script:windowsPackageSources.Add($Name, $moduleSource)
    Save-ModuleSource
    Write-Output -InputObject (New-PackageSourceFromModuleSource -ModuleSource $moduleSource)
}

function Remove-PackageSource
{
    param
    (
        [string]
        $Name
    )
    
    Set-ModuleSourcesVariable -Force

    if(-not $script:windowsPackageSources.Contains($Name))
    {
        Write-Error -Message "Package source $Name not found" `
                        -ErrorId "Package source $Name not found" `
                        -Category InvalidOperation `
                        -TargetObject $Name
        continue
    }

    $script:windowsPackageSources.Remove($Name)

    Save-ModuleSource
}

function New-PackageSourceFromModuleSource
{
    param
    (
        [Parameter(Mandatory=$true)]
        $ModuleSource
    )

    $packageSourceDetails = @{}

    # create a new package source
    $src =  New-PackageSource -Name $ModuleSource.Name `
                              -Location $ModuleSource.SourceLocation `
                              -Trusted $ModuleSource.Trusted `
                              -Registered $ModuleSource.Registered `
                              -Details $packageSourceDetails

    # return the package source object.
    Write-Output -InputObject $src
}

function Get-SourceName
{
    [CmdletBinding()]
    [OutputType("string")]
    Param
    (
        [Parameter(Mandatory=$true)]
        [ValidateNotNullOrEmpty()]
        [string]
        $Location
    )

    Set-ModuleSourcesVariable

    foreach($psModuleSource in $script:windowsPackageSources.Values)
    {
        if(($psModuleSource.Name -eq $Location) -or
           ($psModuleSource.SourceLocation -eq $Location))
        {
            return $psModuleSource.Name
        }
    }
}

#endregion Source

#region OneGet

function Find-Package
{ 
    [CmdletBinding()]
    param
    (
        [string]
        $Name,

        [string]
        $requiredVersion,

        [string]
        $minimumVersion,

        [string]
        $maximumVersion
    )

    $options = $request.Options
    $languageChosen = $null
    $wildcardPattern = $null
    $force = $false
    $allVersions = $false

    # path to the offline nano image
    $imagePath = $null
    $source = $null

    # check out what options the users give us
    if($options)
    {
        foreach( $o in $options.Keys )
        {
            Write-Debug ("OPTION dictionary: {0} => {1}" -f ($o, $options[$o]) )
        }

        if($options.ContainsKey('Force'))
        {
            $force = $options['Force']
        }

        if ($options.ContainsKey("ImagePath"))
        {
            $imagePath = $options['ImagePath']
        }

        if ($options.ContainsKey("Culture"))
        {
            $languageChosen = $options['Culture']
        }

        if ($options.ContainsKey('Source'))
        {
            $source = $options['Source']
        }

        if ($options.ContainsKey('AllVersions'))
        {
            $allVersions = $options['AllVersions']
        }
    }

    <# Commented out because we are not handling source yet
    # no source given then search online
    if ($null -eq $source)
    {
    }
    else
    {
        # If name is null or whitespace, interpret as *
        if ([string]::IsNullOrWhiteSpace($Name))
        {
            $Name = "*"
        }
 
        if ([System.Management.Automation.WildcardPattern]::ContainsWildcardCharacters($Name))
        {
            $wildcardPattern = New-Object System.Management.Automation.WildcardPattern $Name,$script:wildcardOptions
        }
 
        # For now, accept offline source like directory
        if (Test-Path $source)
        {
            $count = 0
            $cabFiles = 0
            $files = Get-ChildItem $source -File
 
            # count number of .cab
            foreach ($file in $files)
            {
                if ([System.IO.Path]::GetExtension($file) -eq '.cab')
                {
                    $cabFiles += 1
                }
            }
 
            if ($cabFiles -le 0)
            {
                return
            }
 
            $id = Write-Progress -ParentId 1 -Activity "Finding packages in $source"
 
            if (-not $id)
            {
                $id = 1
            }
 
            if (-not [string]::IsNullOrWhiteSpace($imagePath))
            {
                if (-not ([System.IO.File]::Exists($ImagePath)))
                {
                    ThrowError -CallerPSCmdlet $PSCmdlet `
                        -ExceptionName System.ArgumentException `
                        -ExceptionMessage "$ImagePath does not exist" `
                        -ExceptionObject $imagePath `
                        -ErrorId "InvalidImagePath" `
                        -ErrorCategory InvalidData
 
                    return
                }
 
                $mountDrive = $null
 
                # have to mount
                $mountDrive = New-MountDrive
         
                Write-Progress -Activity "Mounting $imagePath to $mountDrive" -PercentComplete 0 -Id $id
 
                Mount-WindowsImage -ImagePath $imagePath -Index 1 -Path $mountDrive
 
                try
                {
                    foreach ($file in $files)
                    {
                        if ([System.IO.Path]::GetExtension($file) -eq '.cab')
                        {
                            # scale the percent from 1 to 80 to account for the initial and final step of mounting and dismounting
                            $percentComplete = (($count*80/$cabFiles) + 10) -as [int]
                            $count += 1
 
                            Write-Progress -Activity `
                                "Getting package information for $($package.PackageName) in $mountDrive" `
                                -PercentComplete $percentComplete `
                                -Id $id
 
                            $package = Get-WindowsPackage -PackagePath $file.FullName -Path $mountDrive
 
                            if (Test-PackageWithSearchQuery -fullyQualifiedName $package.PackageName -requiredVersion $RequiredVersion -Name $Name `
                                -minimumVersion $MinimumVersion -maximumVersion $MaximumVersion -Culture $languageChosen -wildCardPattern $wildcardPattern)
                            {
                                Write-Output (New-SoftwareIdentityPackage $package -src $source -InstallLocation $file.FullName)
                            }
                        }
                    }
                }
                finally
                {
                    # time to unmount
                    Write-Progress -Activity "Unmounting image from $mountDrive" -PercentComplete 90 -Id $id
                    Remove-MountDrive $mountDrive
                    Write-Progress -Completed -Id $id -Activity "Completed"
                }
            }
            else
            {
                try
                {
                    #online scenario
                    foreach ($file in $files)
                    {
                        # only checks for .cab extension
                        if ([System.IO.Path]::GetExtension($file) -eq '.cab')
                        {
                            $percentComplete = ($count*100/$cabFiles) -as [int]
                            $count += 1
                                                         
                            Write-Progress -Activity `
                                "Getting package information for $($package.PackageName)" `
                                -PercentComplete $percentComplete `
                                -Id $id
 
                            $package = Get-WindowsPackage -PackagePath $file.FullName -Online
 
                            if (Test-PackageWithSearchQuery -fullyQualifiedName $package.PackageName -requiredVersion $RequiredVersion -Name $Name `
                                -minimumVersion $MinimumVersion -maximumVersion $MaximumVersion -Culture $languageChosen -wildCardPattern $wildcardPattern)
                            {
                                Write-Output (New-SoftwareIdentityPackage $package -src $source -InstallLocation $file.FullName)
                            }
                        }
                    }
                }
                finally
                {
                    Write-Progress -Completed -Id $id -Activity "Completed"
                }
            }
 
        }
        else
        {
            ThrowError -CallerPSCmdlet $PSCmdlet `
                        -ExceptionName "System.ArgumentException" `
                        -ExceptionMessage "Source does not point to a valid directory" `
                        -ErrorId "InvalidSource" `
                        -ErrorCategory InvalidArgument `
                        -ExceptionObject $options
        }
    }
    #>


    # Let find-windowspackage handle the query
    $convertedRequiredVersion = Convert-Version $requiredVersion
    $convertedMinVersion = Convert-Version $minimumVersion
    $convertedMaxVersion = Convert-Version $maximumVersion

    if ([string]::IsNullOrWhiteSpace($Name))
    {
        $Name = @('*')
    }

    $packages = Find-NanoServerPackage -Name $Name `
                                    -Culture $languageChosen `
                                    -RequiredVersion $convertedRequiredVersion `
                                    -MinimumVersion $convertedMinVersion `
                                    -MaximumVersion $convertedMaxVersion `
                                    -AllVersions:$allVersions

    if ($null -eq $packages)
    {
        return
    }

    # check for packages that match the query
    foreach ($package in $packages)
    {
        $swid = New-SoftwareIdentityFromWindowsPackageItemInfo $package
        Write-Output $swid
    }
}

function Install-Package
{
    [CmdletBinding()]
    param
    (
        [string]
        $fastPackageReference
    )

    Write-Verbose $fastPackageReference

    # path to the offline nano image
    $imagePath = $null

    $options = $request.Options

    $NoRestart = $false

    # check out what options the users give us
    if($options)
    {
        foreach( $o in $options.Keys )
        {
            Write-Debug ("OPTION dictionary: {0} => {1}" -f ($o, $options[$o]) )
        }

        if($options.ContainsKey('Force'))
        {
            $force = $options['Force']
        }

        if ($options.ContainsKey("ToVhd"))
        {
            $imagePath = $options['ToVhd']
        }

        if ($options.ContainsKey("Culture"))
        {
            $languageChosen = $options['Culture']
        }

        if ($options.ContainsKey("NoRestart"))
        {
            $NoRestart = $options['NoRestart']
        }
    }

    # if image path is supplied and it points to non existing file, returns
    if (-not [string]::IsNullOrWhiteSpace($imagePath) -and (-not ([System.IO.File]::Exists($ImagePath))))
    {
       ThrowError -CallerPSCmdlet $PSCmdlet `
            -ExceptionName System.ArgumentException `
            -ExceptionMessage "$ImagePath does not exist" `
            -ExceptionObject $imagePath `
            -ErrorId "InvalidImagePath" `
            -ErrorCategory InvalidData

        return
    }

    [string[]] $splitterArray = @("$separator")
    
    [string[]] $resultArray = $fastPackageReference.Split($splitterArray, [System.StringSplitOptions]::None)

    $name = $resultArray[0]
    $version = $resultArray[1]
    #$source = $resultArray[2]
    $Culture = $resultArray[3]

    # if culture is a string, set it to null (this means user did not supply culture)
    if ($Culture.Contains(','))
    {
        $Culture = ''
    }

    $convertedVersion = Convert-Version $version

    [bool]$success = $false

    $mountDrive = $null

    if (-not [string]::IsNullOrWhiteSpace($imagePath))
    {
        $mountDrive = New-MountDrive

        Write-Verbose "Mounting $imagePath to $mountDrive"

        $null = Mount-WindowsImage -ImagePath $imagePath -Index 1 -Path $mountDrive
    }

    try
    {
        $installedPackages = Install-PackageHelper -Name $name `
                                                    -Culture $Culture `
                                                    -Version $convertedVersion `
                                                    -mountDrive $mountDrive `
                                                    -successfullyInstalled ([ref]$success) `
                                                    -NoRestart:$NoRestart

        foreach ($installedPackage in $installedPackages)
        {        
            Write-Output (New-SoftwareIdentityFromWindowsPackageItemInfo ($installedPackage))
        }
    }
    finally
    {
        # unmount
        if ($null -ne $mountDrive)
        {
            Write-Verbose "Unmounting mountdrive $mountDrive"
            Remove-MountDrive $mountDrive -discard (-not $success)
        }
    }
}

function Download-Package
{ 
    [CmdletBinding()]
    param
    (
        [Parameter(Mandatory=$true)]
        [ValidateNotNullOrEmpty()]
        [string]
        $FastPackageReference,

        [Parameter(Mandatory=$true)]
        [ValidateNotNullOrEmpty()]
        [string]
        $Location
    )

    [string[]] $splitterArray = @("$separator")
    
    [string[]] $resultArray = $fastPackageReference.Split($splitterArray, [System.StringSplitOptions]::None)

    $name = $resultArray[0]
    $version = $resultArray[1]
    #$source = $resultArray[2]
    $Culture = $resultArray[3]
    $convertedVersion = Convert-Version $version

    # if culture is a string, set it to null (this means user did not supply culture)
    if ($Culture.Contains(','))
    {
        $Culture = ''
    }

    # no culture given, use culture of the system
    if ([string]::IsNullOrWhiteSpace($Culture))
    {
        $Culture = (Get-Culture).Name
    }

    $force = $false
    $options = $request.Options

    if ($options)
    {
        if ($options.ContainsKey('Force'))
        {
            $force = $options['Force']
        }
    }

    $savedWindowsPackageItems = Save-NanoServerPackage -Name $name `
                                                        -Culture $Culture `
                                                        -RequiredVersion $convertedVersion `
                                                        -Path $Location `
                                                        -Force:$force

    foreach ($savedWindowsPackageItem in $savedWindowsPackageItems)
    {
        Write-Output (New-SoftwareIdentityFromWindowsPackageItemInfo $savedWindowsPackageItem)
    }
}

function Get-InstalledPackage
{
    [CmdletBinding()]
    param
    (
        [Parameter()]
        [string]
        $Name,

        [Parameter()]
        [Version]
        $RequiredVersion,

        [Parameter()]
        [Version]
        $MinimumVersion,

        [Parameter()]
        [Version]
        $MaximumVersion
    )

    $options = $request.Options
    $wildcardPattern = $null
    $languageChosen = $null

    # If name is null or whitespace, interpret as *
    if ([string]::IsNullOrWhiteSpace($Name))
    {
        $Name = "*"
    }

    if ([System.Management.Automation.WildcardPattern]::ContainsWildcardCharacters($Name))
    {
        $wildcardPattern = New-Object System.Management.Automation.WildcardPattern $Name,$script:wildcardOptions
    }

    $force = $false

    # path to the offline nano image
    $imagePath = $null

    # check out what options the users give us
    if($options)
    {
        foreach( $o in $options.Keys )
        {
            Write-Debug ("OPTION dictionary: {0} => {1}" -f ($o, $options[$o]) )
        }

        if($options.ContainsKey('Force'))
        {
            $force = $options['Force']
        }

        if ($options.ContainsKey("FromVhd"))
        {
            $imagePath = $options['FromVhd']
        }
        elseif ($options.ContainsKey("ToVhd"))
        {
            # in case of install
            $imagePath = $options['ToVhd']
        }

        if ($options.ContainsKey("Culture"))
        {
            $languageChosen = $options['Culture']

            $cannotConvertCulture = $false

            # try to convert the culture
            try
            {
                $convertedCulture = [cultureinfo]$languageChosen

                # apparently, converting culture 'blah' will not work but 'bla' will work ?!?
                if ($null -eq $convertedCulture -or $null -eq $convertedCulture.DisplayName -or $convertedCulture.DisplayName.Trim() -match "Unknown Language")
                {
                    $cannotConvertCulture = $true
                }
            }
            catch
            {
                $cannotConvertCulture = $true
            }

            # if we cannot convert culture, throw error
            if ($cannotConvertCulture)
            {
                ThrowError -CallerPSCmdlet $PSCmdlet `
                            -ExceptionName System.ArgumentException `
                            -ExceptionMessage "$languageChosen is not a valid culture" `
                            -ExceptionObject $languageChosen `
                            -ErrorId InvalidCulture `
                            -ErrorCategory InvalidData
            }
        }
    }

    if (-not [string]::IsNullOrWhiteSpace($imagePath))
    {
        $mountDrive = New-MountDrive   

        Write-Verbose "Mounting $imagePath to $mountDrive"

        $id = Write-Progress -ParentId 1 -Activity "Getting packages information"
        if (-not $id)
        {
            $id = 1
        }

        Write-Progress -Activity "Mounting $imagePath to $mountDrive" -PercentComplete 0 -Id $id

        Mount-WindowsImage -ImagePath $imagePath -Index 1 -Path $mountDrive

        Write-Verbose "Done Mounting"

        # Now we can try to find the packages
        try
        {
            # Get all the available packages on the mountdrive
            $packages = Get-WindowsPackage -Path $mountDrive
            Write-Verbose "Finished getting packages from $mountDrive with $($packages.Count) packages"
            $count = 0

            Write-Progress -Activity "Getting packages information from $mountDrive" -PercentComplete 5 -Id $id
            
            $packagesToBeReturned = New-Object 'System.Collections.Generic.List[string]'

            $availablePackages = $packages.PackageName.ToLower()

            # check for packages that match the query
            foreach ($fullyQualifiedName in $availablePackages)
            {
                if (Test-PackageWithSearchQuery -fullyQualifiedName $fullyQualifiedName -requiredVersion $RequiredVersion -Name $Name `
                    -minimumVersion $MinimumVersion -maximumVersion $MaximumVersion -Culture $languageChosen -wildCardPattern $wildcardPattern)
                {
                    $packagesToBeReturned.Add($fullyQualifiedName)
                }
            }

            $fileKey = Get-FileKey -filePath $imagePath

            $packageDictionary = @{}

            # try to get the cache if it exists, otherwise create one
            if (-not $script:imagePathCache.ContainsKey($fileKey))
            {                    
                $script:imagePathCache.Add($fileKey, $packageDictionary)
            }

            $packageDictionary = $script:imagePathCache[$fileKey]

            # Before we get more details, we will clump together base and language pack if they have same name and version
            $packagesToBeReturned = Filter-Packages $packagesToBeReturned

            foreach ($package in $packagesToBeReturned)
            {
                # scale the percent from 1 to 80 to account for the initial and final step of mounting and dismounting
                $percentComplete = (($count*80/$packages.Count) + 10) -as [int]
                $count += 1

                Write-Progress -Activity `
                    "Getting package information for $package in $mountDrive" `
                    -PercentComplete $percentComplete `
                    -Id $id                

                # store the information in cache if it's not there or if user uses force
                if (-not $packageDictionary.ContainsKey($package) -or $force)
                {
                    Write-Debug "Getting information for package $package and storing it in cache"
                    # store the information in cache
                    $packageDictionary[$package.ToLower()] = Get-WindowsPackage -PackageName $package -Path $mountDrive
                }

                Write-Output (New-SoftwareIdentityPackage $packageDictionary[$package.ToLower()] -src $imagePath)
            }

            # Get the list of packages that are in the cache but not in the latest list we have
            $packageToBeRemoved = @()
            foreach ($pkg in $packageDictionary.GetEnumerator())
            {
                if (-not $availablePackages.Contains($pkg.Name))
                {
                    $packageToBeRemoved += $pkg.Name
                }
            }

            # Remove packages in this list from the cache
            foreach ($pkg in $packageToBeRemoved)
            {
                if ($packageDictionary.ContainsKey($pkg))
                {
                    $packageDictionary.Remove($pkg)
                }
            }
        }
        finally
        {
            Write-Progress -Activity "Unmounting image from $mountDrive" -PercentComplete 90 -Id $id
            # Unmount and delete directory
            Remove-MountDrive $mountDrive
            Write-Progress -Completed -Id $id -Activity "Completed"
        }
    }
    else
    {
        $count = 0;
        $id = Write-Progress -ParentId 1 -Activity "Getting packages information"
        if (-not $id)
        {
            $id = 1
        }
        
        Write-Progress -Activity "Getting available packages on the system" -PercentComplete 0 -Id $id
        # getting the packages on the current operating system
        # getting basic information about all the packages online
        $packages = Get-WindowsPackage -Online

        try
        {
            $packagesToBeReturned = New-Object 'System.Collections.Generic.List[string]'
            $availablePackages = $packages.PackageName.ToLower()

            # Get the list of packages that match what the user input
            foreach ($fullyQualifiedName in $availablePackages)
            {
                if (Test-PackageWithSearchQuery -fullyQualifiedName $fullyQualifiedName -requiredVersion $RequiredVersion -Name $Name `
                    -minimumVersion $MinimumVersion -maximumVersion $MaximumVersion -Culture $languageChosen -wildCardPattern $wildCardPattern)
                {
                    # Store the whole name instead of just the name without language or version
                    $packagesToBeReturned.Add($fullyQualifiedName)
                }
            }

            # nothing matched!
            if ($packagesToBeReturned.Count -eq 0)
            {
                return
            }

            # Before we get more details, we will clump together base and language pack if they have same name and version
            $packagesToBeReturned = Filter-Packages $packagesToBeReturned

            # Only update the list of packages that the user gives
            foreach ($package in $packagesToBeReturned)
            {
                $percentComplete = ($count*90/$packages.Count + 10) -as [int]
                Write-Progress -Activity "Getting package information for $($package)" -PercentComplete $percentComplete -Id $id
                $count += 1;

                # store the information in cache if it's not there or if user uses force
                if (-not $script:onlinePackageCache.ContainsKey($package) -or $force)
                {
                    Write-Debug "Getting information for package $package and storing it in cache"
                    # store the information in cache
                    $script:onlinePackageCache[$package.ToLower()] = Get-WindowsPackage -Online -PackageName $package
                }

                if ($script:onlinePackageCache.ContainsKey($package))
                {
                    # convert package to swid and return
                    Write-Output (New-SoftwareIdentityPackage $script:onlinePackageCache[$package] -src "Local Machine")
                }
            }
            
            # Get the list of packages that are in the cache but not in the latest list we have
            $packageToBeRemoved = @()
            foreach ($pkg in $script:onlinePackageCache.GetEnumerator())
            {
               if (-not $availablePackages.Contains($pkg.Name))
                {
                    $packageToBeRemoved += $pkg.Name
                }
            }

            # Remove packages in this list from the cache
            foreach ($pkg in $packageToBeRemoved)
            {
                if ($script:onlinePackageCache.ContainsKey($pkg))
                {
                    $script:onlinePackageCache.Remove($pkg)
                }
            }
        }
        finally 
        {
            Write-Progress -Completed -Id $id -Activity "Completed"
        }
    }
}

#endregion OneGet

#region OneGet Helpers

# This is to display long name
function Get-Feature 
{
    Write-Output -InputObject (New-Feature -Name "DisplayLongName")
}

function Get-DynamicOptions
{
    param
    (
        [Microsoft.PackageManagement.MetaProvider.PowerShell.OptionCategory] 
        $category
    )

    switch($category)
    {
        # This is for dynamic options used by install/uninstall and get-packages
        Install 
        {
            # Switch to display culture
            Write-Output -InputObject (New-DynamicOption -Category $Category -Name "NoRestart" -ExpectedType Switch -IsRequired $false)
            # Provides path to image
            Write-Output -InputObject (New-DynamicOption -Category $category -Name "ToVhd" -ExpectedType File -IsRequired $false)
            Write-Output -InputObject (New-DynamicOption -Category $category -Name "FromVhd" -ExpectedType File -IsRequired $false)
            Write-Output -InputObject (New-DynamicOption -Category $Category -Name "DisplayCulture" -ExpectedType Switch -IsRequired $false)
            Write-Output -InputObject (New-DynamicOption -Category $category -Name "Culture" -ExpectedType String -IsRequired $false)
        }
        Package
        {
            # Switch to display culture
            Write-Output -InputObject (New-DynamicOption -Category $Category -Name "DisplayCulture" -ExpectedType Switch -IsRequired $false)
            # Provides path to image
            Write-Output -InputObject (New-DynamicOption -Category $category -Name "ImagePath" -ExpectedType String -IsRequired $false)
            Write-Output -InputObject (New-DynamicOption -Category $category -Name "Culture" -ExpectedType String -IsRequired $false)
        }
        Source
        {
            Write-Output -InputObject (New-DynamicOption -Category $Category -Name "Default" -ExpectedType Switch -IsRequired $false)
        }
    }
}

function Initialize-Provider
{
    write-debug "In $script:providerName - Initialize-Provider"
}

function Get-PackageProviderName
{
    return $script:providerName
}

function New-SoftwareIdentityFromWindowsPackageItemInfo
{
    [Cmdletbinding()]
    param(
        [PSCustomObject]
        $package
    )

    $details = @{}
    $Culture = $package.Culture

    $fastPackageReference = $package.Name + 
                                $separator + $package.version + 
                                $separator + $package.Source + 
                                $separator + $Culture

    $Name = [System.IO.Path]::GetFileNameWithoutExtension($package.Name)

    $params = @{FastPackageReference = $fastPackageReference;
                Name = $Name;
                Version = $package.version.ToString();
                versionScheme  = "MultiPartNumeric";
                Source = $package.Source;
                Summary = $package.Description;
                Details = $details;
                Culture = $Culture
                }

    try
    {
        New-SoftwareIdentity @params
    }
    catch
    {
        # throw error because older version of packagemanagement does not have culture key
        $params.Remove("Culture")
        New-SoftwareIdentity @params
    }
}

function New-SoftwareIdentityPackage
{
    [CmdletBinding()]
    param(
        [Parameter(Mandatory=$true)]
        [Microsoft.Dism.Commands.AdvancedPackageObject]
        $package,

        $src="",

        $InstallLocation=""
    )

    $details = @{}

    $details.Add("Applicable", $package.Applicable)

    if ($null -ne $package.InstallTime)
    {
        $details.Add("InstallTime", $package.InstallTime)
    }

    if ($null -ne $package.CompletelyOfflineCapable)
    {
        $details.Add("CompletelyOfflineCapable", $package.CompletelyOfflineCapable)
    }

    if ($null -ne $package.PackageState)
    {
        $details.Add("PackageState", $package.PackageState)
    }        

    if ($null -ne $package.RestartRequired)
    {
        $details.Add("RestartRequired", $package.RestartRequired)
    }

    if (-not [string]::IsNullOrWhiteSpace($package.ReleaseType))
    {
        $details.Add("ReleaseType", $package.ReleaseType)
    }        

    if ([string]::IsNullOrWhiteSpace($Package.ProductVersion)) {
        $version = "0.0"
    }
    else {
        $version = $Package.ProductVersion
    }

    # format is name~publickeytoken~architecture~language~version

    $packageNameFractions = $Package.PackageName.Split('~')

    if (-not [string]::IsNullOrWhiteSpace($packageNameFractions[0]))
    {
        $name = $packageNameFractions[0]
    }
    else
    {
        $name = $package.PackageName
    }

    # DISM team has a workaround where they add Feature in the name. We should remove that.
    # THIS IS A TEMPORARY FIX
    if ($name -like "*Feature-Package*")
    {
        $name = $name -replace "Feature-Package","Package"
    }

    if (-not [string]::IsNullOrWhiteSpace($packageNameFractions[1]))
    {
        $details.Add("publickey", $packageNameFractions[1])
    }

    if (-not [string]::IsNullOrWhiteSpace($packageNameFractions[2]))
    {
        $details.Add("architecture", $packageNameFractions[2])
    }

    $Culture = $packageNameFractions[3]
    
    # $details.Add("language", $language)

    if (-not [string]::IsNullOrWhiteSpace($packageNameFractions[4]))
    {
        $version = $packageNameFractions[4]
    }

    $fastPackageReference = $name + $separator + $Culture + $separator + $version + $separator + $InstallLocation

    $params = @{FastPackageReference = $fastPackageReference;
                Name = $name;
                Version = $version;
                versionScheme  = "MultiPartNumeric";
                Source = $src;
                Details = $details;
                Culture = $Culture
                }

    try
    {
        New-SoftwareIdentity @params
    }
    catch
    {
        # throw error because older version of packagemanagement does not have culture key
        $params.Remove("Culture")
        New-SoftwareIdentity @params
    }
}

function Install-CabOfflineFromPath
{
    [CmdletBinding()]
    param
    (
        [string]$mountDrive,

        [string[]]$packagePaths
    )

    $discard = $false

    $id = Write-Progress -ParentId 1 -Activity "Installing packages"

    if (-not $id)
    {
        $id = 1
    }  

    # Now we can try to install the package
    try
    {
        $count = 0

        foreach ($packagePath in $packagePaths)
        {
            $percentComplete = ($count*100/$packagePaths.Count) -as [int]

            $count += 1

            Write-Progress -Activity `
                "Installing package $packagePath" `
                -PercentComplete $percentComplete `
                -Id $id                

            Write-Verbose "Adding $packagePath to $mountDrive"
            Add-WindowsPackage -PackagePath $packagePath -Path $mountDrive -NoRestart -WarningAction Ignore | Out-Null
        }
    }
    catch
    {
        $discard = $true
        ThrowError -CallerPSCmdlet $PSCmdlet `
                    -ExceptionName $_.Exception.GetType().FullName `
                    -ExceptionMessage $_.Exception.Message `
                    -ExceptionObject $RequiredVersion `
                    -ErrorId FailedToInstall `
                    -ErrorCategory InvalidOperation
    }
    finally
    {
        Write-Progress -Completed -Id $id -Activity "Completed"
    }

    # returns back whether we have successfully installed or not
    return (-not $discard)
}

function Install-Online
{
    [CmdletBinding()]
    param
    (
        [string[]]$packagePaths,
        [ref]$restartNeeded
    )

    $installedPackages = @()
    $rollBack = $false

    $count = 0;
    Write-Verbose "Installing $($packagePaths.Count) packages online"
    $id = Write-Progress -ParentId 1 -Activity "Installing packages online"
    if (-not $id)
    {
        $id = 1
    }

    try
    {
        foreach ($packagePath in $packagePaths)
        {
            $percentComplete = $count*100/$packagePaths.Count -as [int]
            Write-Progress -Activity "Installing package $($packagePath)" -PercentComplete $percentComplete -Id $id
            $count += 1;

            $messages = Add-WindowsPackage -PackagePath $packagePath -Online -NoRestart -WarningAction Ignore
    
            # restart or not
            if ($messages.RestartNeeded)
            {
                $restartNeeded.Value = $true
            }

            $installedPackages += $packagePath
        }
    }
    catch
    {
        $rollBack = $true
        ThrowError -CallerPSCmdlet $PSCmdlet `
                    -ExceptionName $_.Exception.GetType().FullName `
                    -ExceptionMessage $_.Exception.Message `
                    -ExceptionObject $RequiredVersion `
                    -ErrorId FailedToInstall `
                    -ErrorCategory InvalidOperation
    }
    finally
    {
        Write-Progress -Completed -Id $id -Activity "Completed"
    }

    if ($rollBack)
    {    
        Write-Verbose "Installation fails, we need to rollback"
        $id = Write-Progress -ParentId 1 -Activity "Rolling back installed packages"
        if (-not $id)
        {
            $id = 1
        }
        
        $count = 0

        try
        {
            foreach ($installedPackage in $installedPackages)
            {
                $percentComplete = $count*100/$installedPackages.Count -as [int]
                Write-Progress -Activity "Uninstalling $installedPackage" -PercentComplete $percentComplete -Id $id
                $count += 1;

                Write-Verbose "Uninstalling package $installedPackage"
                Remove-WindowsPackage -PackagePath $installedPackage -Online
            }
        }
        catch
        {
            $rollBack = $true
            ThrowError -CallerPSCmdlet $PSCmdlet `
                        -ExceptionName $_.Exception.GetType().FullName `
                        -ExceptionMessage $_.Exception.Message `
                        -ExceptionObject $RequiredVersion `
                        -ErrorId FailedToUnInstall `
                        -ErrorCategory InvalidOperation
        }
        finally
        {
            Write-Progress -Completed -Id $id -Activity "Completed"
        }
    }

    # returns whether we installed
    return (-not $rollBack)
}

function New-MountDrive
{
    # getting packages from an offline image
    # Mount to directory
    while ($true)
    {
        $randomName = [System.IO.Path]::GetRandomFileName()
        $mountDrive = "$env:LOCALAPPDATA\NanoServerPackageProvider\MountDirectories\$randomName"
            
        if (Test-Path $mountDrive) 
        {
            # We should create a directory that hasn't existed before
            continue;
        }
        else 
        {
            $null = mkdir $mountDrive
            return $mountDrive
        }
    }
}

function Remove-MountDrive([string]$mountDrive, [bool]$discard)
{    
    Write-Verbose "Dismounting $mountDrive"

    # Discard won't save anything we did to the image
    if ($discard)
    {
        $null = Dismount-WindowsImage -Path $mountDrive -Discard
    }
    else
    {
    # save will saves packages that we add to the image
        $null = Dismount-WindowsImage -Path $mountDrive -Save
    }

    Write-Verbose "Deleting $mountDrive"
    Remove-Item -Path $mountDrive -Recurse -Force
}

# Given a fully qualified name of a package with the format name~publickeytoken~architecture~language~version
# checks whether this matches the search query
function Test-PackageWithSearchQuery
{
    [CmdletBinding()]
    param
    (
        [Parameter(ParameterSetName="FullyQualifiedName")]
        [string]$fullyQualifiedName,

        [Parameter(ParameterSetName="WindowsPackage")]
        [PSCustomObject]$WindowsPackage,

        [string]$requiredVersion,

        [string]$minimumVersion,

        [string]$maximumVersion,

        [string]$name,

        [string]$Culture,

        [System.Management.Automation.WildcardPattern]$wildCardPattern
    )

    if ($null -eq $WindowsPackage)
    {
        # Split up the whole name since the name has language version and packagename in it
        # format is name~publickeytoken~architecture~language~version
        # now we want the package name to have en-us at the end if package is not base
        $packageNameFractions = $fullyQualifiedName.Split('~')
        $packageName = $packageNameFractions[0]
        $packageLanguage = $packageNameFractions[3]
        $version = $packageNameFractions[4]

        # DISM team has a workaround where they add Feature in the name. We should remove that.
        # THIS IS A TEMPORARY FIX
        if ($packageName -like "*Feature-Package")
        {
            $packageName = $packageName -replace "Feature-Package", "Package"
        }
    }
    else
    {
        $packageName = $WindowsPackage.Name
        $packageLanguage = $WindowsPackage.Culture
        $version = $WindowsPackage.version.ToString()
    }

    # there is a chance user supplies *<PackageLanguage>
    if (-not [string]::IsNullOrWhiteSpace($packageLanguage))
    {
        $packageNameWithLanguage = "$packageName" + "_" + "$packageLanguage"
    }   

    if ($null -ne $wildCardPattern)
    {
        # matching already ignore case
        if (-not $wildCardPattern.IsMatch($packageName))
        {
            # we proceed if wildcard match <PackageName>_<PackageLanguage>
            if (-not [string]::IsNullOrWhiteSpace($packageLanguage))
            {
                if (-not $wildCardPattern.IsMatch($packageLanguage))
                {
                    return $false
                }
            }
            else
            {
                return $false
            }
        }
    }
    else
    {
        # no wildcard so check for name if we are given a name
        # eq operation is case insensitive
        if (-not [string]::IsNullOrWhiteSpace($name) -and $name -ne $packageName)
        {
            # there is a chance user supplies <PackageName>_<PackageLanguage>
            if (-not [string]::IsNullOrWhiteSpace($packageLanguage))
            {
                # we proceed if name match <PackageName>_<PackageLanguage>
                if ($name -ne $packageNameWithLanguage)
                {
                    return $false
                }
            }
            else
            {
                return $false
            }
        }
    }

    # now we check language if the user providers it
    if (-not [string]::IsNullOrWhiteSpace($Culture))
    {
        # if base, then packageLanguage needs to be null
        if ($Culture -eq 'Base')
        {
            $Culture = ''
        }

        if ($packageLanguage -ne $Culture)
        {
            return $false
        }
    }

    # normalize versions
    $convertedVersion = Convert-Version $version

    # fails to normalize
    if ($null -eq $convertedVersion)
    {
        return $false
    }

    # now we check whether version is matched
    if (-not [string]::IsNullOrWhiteSpace($RequiredVersion))
    {
        $convertedRequiredVersion = Convert-Version $RequiredVersion

        # fails if conversion fails or version does not match
        if (($null -eq $convertedRequiredVersion) -or ($convertedRequiredVersion -ne $convertedVersion))
        {
            return $false
        }
    }

    # packagemanagement will make sure requiredversion is not used with either min or max so we don't have to worry about that
    if (-not [string]::IsNullOrWhiteSpace($MinimumVersion))
    {
        $convertedMinimumVersion = Convert-Version $MinimumVersion

        # the converted version should be greater or equal to min version, not the other way round
        if (($null -eq $convertedMinimumVersion) -or ($convertedMinimumVersion -gt $convertedVersion))
        {
            return $false
        }
    }

    if (-not [string]::IsNullOrWhiteSpace($MaximumVersion))
    {
        $convertedMaximumVersion = Convert-Version $MaximumVersion

        # converted version should be the same or less than max version
        if (($null -eq $convertedMaximumVersion) -or ($convertedMaximumVersion -lt $convertedVersion))
        {
            return $false
        }
    }

    # reached here means the package satisfied the search query
    return $true
}

# Given the path of a file, returns a simple key
# This function assumes that the user test that filePath exists
function Get-FileKey([string]$filePath)
{
    $info = Get-ChildItem $filePath
    return ($filePath + $separator + $info.Length + $separator + $info.CreationTime.ToShortDateString())
}

function Convert-Version([string]$version)
{
    if ([string]::IsNullOrWhiteSpace($version))
    {
        return $null;
    }

    # not supporting semver here. let's try to normalize the versions
    if ($version.StartsWith("."))
    {
        # add leading zeros
        $version = "0" + $version
    }
        
    # let's see how many parts are we given with the version
    $parts = $version.Split(".").Count

    # add .0 dependending number of parts since we need 4 parts
    while ($parts -lt 4)
    {
        $version = $version + ".0"
        $parts += 1
    }

    [version]$convertedVersion = $null

    # try to convert
    if ([version]::TryParse($version, [ref]$convertedVersion))
    {
        return $convertedVersion
    }

    return $null;
}

function Filter-Packages ([string[]]$packagesToBeReturned)
{
    $helperDictionary = @{}
    foreach ($package in $packagesToBeReturned)
    {
        # Split up the whole name since the name has language version and packagename in it
        # format is name~publickeytoken~architecture~language~version
        # now we want the package name to have en-us at the end if package is not base
        $packageNameFractions = $package.Split('~')
        $packageName = $packageNameFractions[0]
        $packageLanguage = $packageNameFractions[3]
        $version = $packageNameFractions[4]
        
        # use name and version as key
        $key = $packageName + "~" + $version

        # haven't encountered this before
        if (-not $helperDictionary.ContainsKey($key))
        {
            $helperDictionary[$key] = @()
        }

        $helperDictionary[$key] += $package
    }

    $result = @()

    foreach ($packageArray in $helperDictionary.Values)
    {
        if ($null -eq $packageArray)
        {
            continue
        }

        # only 1 member, then return that
        if ($packageArray.Count -eq 1)
        {
            $result += $packageArray[0]
            continue
        }

        # otherwise, only returns the 1 with language
        foreach ($possiblePackage in $packageArray)
        {
            $packageNameFractions = $possiblePackage.Split('~')
            $packageName = $packageNameFractions[0]
            $packageLanguage = $packageNameFractions[3]
            $version = $packageNameFractions[4]

            if ([string]::IsNullOrWhiteSpace($packageLanguage))
            {
                continue
            }

            $result += $possiblePackage
        }
    }

    # group according to name
    $groupedName = $result | Group-Object -Property {$_.Split('~')[0]}
    foreach ($groupResult in $groupedName)
    {
        $groupResult.Group | Sort-Object -Property {Convert-Version $_.Split('~')[4]} -Descending
    }
}

# Utility to throw an errorrecord
function ThrowError
{
    param
    (        
        [parameter(Mandatory = $true)]
        [ValidateNotNullOrEmpty()]
        [System.Management.Automation.PSCmdlet]
        $CallerPSCmdlet,

        [parameter(Mandatory = $true)]
        [ValidateNotNullOrEmpty()]
        [System.String]        
        $ExceptionName,

        [parameter(Mandatory = $true)]
        [ValidateNotNullOrEmpty()]
        [System.String]
        $ExceptionMessage,
        
        [System.Object]
        $ExceptionObject,
        
        [parameter(Mandatory = $true)]
        [ValidateNotNullOrEmpty()]
        [System.String]
        $ErrorId,

        [parameter(Mandatory = $true)]
        [ValidateNotNull()]
        [System.Management.Automation.ErrorCategory]
        $ErrorCategory
    )
        
    $exception = New-Object $ExceptionName $ExceptionMessage;
    $errorRecord = New-Object System.Management.Automation.ErrorRecord $exception, $ErrorId, $ErrorCategory, $ExceptionObject    
    $CallerPSCmdlet.ThrowTerminatingError($errorRecord)
}

#endregion OneGet Helpers

# SIG # Begin signature block
# MIIarwYJKoZIhvcNAQcCoIIaoDCCGpwCAQExCzAJBgUrDgMCGgUAMGkGCisGAQQB
# gjcCAQSgWzBZMDQGCisGAQQBgjcCAR4wJgIDAQAABBAfzDtgWUsITrck0sYpfvNR
# AgEAAgEAAgEAAgEAAgEAMCEwCQYFKw4DAhoFAAQUJ409ylcXgskZrYDe/L1iiy6V
# 09+gghWCMIIEwzCCA6ugAwIBAgITMwAAAJb6gDHvN2RGRQAAAAAAljANBgkqhkiG
# 9w0BAQUFADB3MQswCQYDVQQGEwJVUzETMBEGA1UECBMKV2FzaGluZ3RvbjEQMA4G
# A1UEBxMHUmVkbW9uZDEeMBwGA1UEChMVTWljcm9zb2Z0IENvcnBvcmF0aW9uMSEw
# HwYDVQQDExhNaWNyb3NvZnQgVGltZS1TdGFtcCBQQ0EwHhcNMTUxMDA3MTgxNDI0
# WhcNMTcwMTA3MTgxNDI0WjCBszELMAkGA1UEBhMCVVMxEzARBgNVBAgTCldhc2hp
# bmd0b24xEDAOBgNVBAcTB1JlZG1vbmQxHjAcBgNVBAoTFU1pY3Jvc29mdCBDb3Jw
# b3JhdGlvbjENMAsGA1UECxMETU9QUjEnMCUGA1UECxMebkNpcGhlciBEU0UgRVNO
# OkJCRUMtMzBDQS0yREJFMSUwIwYDVQQDExxNaWNyb3NvZnQgVGltZS1TdGFtcCBT
# ZXJ2aWNlMIIBIjANBgkqhkiG9w0BAQEFAAOCAQ8AMIIBCgKCAQEAm1pYSwjyVGa6
# tIZe8M6+zXQQ33WKYIyKYcI3oiZcZgVcxdizVjv3hKmjqmRTC5REuLtaSYbdeCuG
# bdMP2+NGWrqeWKLQIxb/Gs/BkEzrr+ewnZ+UQ7xON8jkhPhMSdT5ZiVVNdhVgo+y
# 3hvrk0tk4iDpr5Xwqk5U2W5yZkXras/mIIfO54mjfS31tKQbIsxxubm8Np9ioBit
# boqgiC1iwSxGh7/LGPp1NJVacuQc1JMuzkhRNXxwALbWbyrsUV8Aztz5eaUASLoF
# jkK43ety0X/rV9Qlws43Q2LjKhztpEaxloEr0gioCAEmkJssDjd1qqCZ6X/bht1e
# ggluXnz2tQIDAQABo4IBCTCCAQUwHQYDVR0OBBYEFMfD/XvxW9NCtvwEw94qmvuS
# ht7IMB8GA1UdIwQYMBaAFCM0+NlSRnAK7UD7dvuzK7DDNbMPMFQGA1UdHwRNMEsw
# SaBHoEWGQ2h0dHA6Ly9jcmwubWljcm9zb2Z0LmNvbS9wa2kvY3JsL3Byb2R1Y3Rz
# L01pY3Jvc29mdFRpbWVTdGFtcFBDQS5jcmwwWAYIKwYBBQUHAQEETDBKMEgGCCsG
# AQUFBzAChjxodHRwOi8vd3d3Lm1pY3Jvc29mdC5jb20vcGtpL2NlcnRzL01pY3Jv
# c29mdFRpbWVTdGFtcFBDQS5jcnQwEwYDVR0lBAwwCgYIKwYBBQUHAwgwDQYJKoZI
# hvcNAQEFBQADggEBADQzONHGQV0X/NPCsvaZQv26Syn1rUGW85E9wUCgtf0iWG55
# ntOcHryYkkVIkjB/vd9ixfzGlW2Bz08YdPHJc5he9ZNkfwhjHqW9r6ii06pa4kzE
# PbgYlLwVRRvxzJwLZpSe56UceM8FmEnsRUSVKzabhLjmiIAFpnNlGgYd6g0eDvxT
# FM9SOJozV4Mjyb7e+Gv//ZxUeZcTK2S/Nam+B6m/mlRVajUYotCDwziVxrm1irMt
# a15M55pT3aawt+QrwXaRUMRSRmIgXTHgFWdM3AksQGA0a77rRKGYldX0iPyH2XOw
# rTHQww9kEcX1r+2R+9QjmsljYc3ZPGnA+2YCADEwggTsMIID1KADAgECAhMzAAAB
# Cix5rtd5e6asAAEAAAEKMA0GCSqGSIb3DQEBBQUAMHkxCzAJBgNVBAYTAlVTMRMw
# EQYDVQQIEwpXYXNoaW5ndG9uMRAwDgYDVQQHEwdSZWRtb25kMR4wHAYDVQQKExVN
# aWNyb3NvZnQgQ29ycG9yYXRpb24xIzAhBgNVBAMTGk1pY3Jvc29mdCBDb2RlIFNp
# Z25pbmcgUENBMB4XDTE1MDYwNDE3NDI0NVoXDTE2MDkwNDE3NDI0NVowgYMxCzAJ
# BgNVBAYTAlVTMRMwEQYDVQQIEwpXYXNoaW5ndG9uMRAwDgYDVQQHEwdSZWRtb25k
# MR4wHAYDVQQKExVNaWNyb3NvZnQgQ29ycG9yYXRpb24xDTALBgNVBAsTBE1PUFIx
# HjAcBgNVBAMTFU1pY3Jvc29mdCBDb3Jwb3JhdGlvbjCCASIwDQYJKoZIhvcNAQEB
# BQADggEPADCCAQoCggEBAJL8bza74QO5KNZG0aJhuqVG+2MWPi75R9LH7O3HmbEm
# UXW92swPBhQRpGwZnsBfTVSJ5E1Q2I3NoWGldxOaHKftDXT3p1Z56Cj3U9KxemPg
# 9ZSXt+zZR/hsPfMliLO8CsUEp458hUh2HGFGqhnEemKLwcI1qvtYb8VjC5NJMIEb
# e99/fE+0R21feByvtveWE1LvudFNOeVz3khOPBSqlw05zItR4VzRO/COZ+owYKlN
# Wp1DvdsjusAP10sQnZxN8FGihKrknKc91qPvChhIqPqxTqWYDku/8BTzAMiwSNZb
# /jjXiREtBbpDAk8iAJYlrX01boRoqyAYOCj+HKIQsaUCAwEAAaOCAWAwggFcMBMG
# A1UdJQQMMAoGCCsGAQUFBwMDMB0GA1UdDgQWBBSJ/gox6ibN5m3HkZG5lIyiGGE3
# NDBRBgNVHREESjBIpEYwRDENMAsGA1UECxMETU9QUjEzMDEGA1UEBRMqMzE1OTUr
# MDQwNzkzNTAtMTZmYS00YzYwLWI2YmYtOWQyYjFjZDA1OTg0MB8GA1UdIwQYMBaA
# FMsR6MrStBZYAck3LjMWFrlMmgofMFYGA1UdHwRPME0wS6BJoEeGRWh0dHA6Ly9j
# cmwubWljcm9zb2Z0LmNvbS9wa2kvY3JsL3Byb2R1Y3RzL01pY0NvZFNpZ1BDQV8w
# OC0zMS0yMDEwLmNybDBaBggrBgEFBQcBAQROMEwwSgYIKwYBBQUHMAKGPmh0dHA6
# Ly93d3cubWljcm9zb2Z0LmNvbS9wa2kvY2VydHMvTWljQ29kU2lnUENBXzA4LTMx
# LTIwMTAuY3J0MA0GCSqGSIb3DQEBBQUAA4IBAQCmqFOR3zsB/mFdBlrrZvAM2PfZ
# hNMAUQ4Q0aTRFyjnjDM4K9hDxgOLdeszkvSp4mf9AtulHU5DRV0bSePgTxbwfo/w
# iBHKgq2k+6apX/WXYMh7xL98m2ntH4LB8c2OeEti9dcNHNdTEtaWUu81vRmOoECT
# oQqlLRacwkZ0COvb9NilSTZUEhFVA7N7FvtH/vto/MBFXOI/Enkzou+Cxd5AGQfu
# FcUKm1kFQanQl56BngNb/ErjGi4FrFBHL4z6edgeIPgF+ylrGBT6cgS3C6eaZOwR
# XU9FSY0pGi370LYJU180lOAWxLnqczXoV+/h6xbDGMcGszvPYYTitkSJlKOGMIIF
# vDCCA6SgAwIBAgIKYTMmGgAAAAAAMTANBgkqhkiG9w0BAQUFADBfMRMwEQYKCZIm
# iZPyLGQBGRYDY29tMRkwFwYKCZImiZPyLGQBGRYJbWljcm9zb2Z0MS0wKwYDVQQD
# EyRNaWNyb3NvZnQgUm9vdCBDZXJ0aWZpY2F0ZSBBdXRob3JpdHkwHhcNMTAwODMx
# MjIxOTMyWhcNMjAwODMxMjIyOTMyWjB5MQswCQYDVQQGEwJVUzETMBEGA1UECBMK
# V2FzaGluZ3RvbjEQMA4GA1UEBxMHUmVkbW9uZDEeMBwGA1UEChMVTWljcm9zb2Z0
# IENvcnBvcmF0aW9uMSMwIQYDVQQDExpNaWNyb3NvZnQgQ29kZSBTaWduaW5nIFBD
# QTCCASIwDQYJKoZIhvcNAQEBBQADggEPADCCAQoCggEBALJyWVwZMGS/HZpgICBC
# mXZTbD4b1m/My/Hqa/6XFhDg3zp0gxq3L6Ay7P/ewkJOI9VyANs1VwqJyq4gSfTw
# aKxNS42lvXlLcZtHB9r9Jd+ddYjPqnNEf9eB2/O98jakyVxF3K+tPeAoaJcap6Vy
# c1bxF5Tk/TWUcqDWdl8ed0WDhTgW0HNbBbpnUo2lsmkv2hkL/pJ0KeJ2L1TdFDBZ
# +NKNYv3LyV9GMVC5JxPkQDDPcikQKCLHN049oDI9kM2hOAaFXE5WgigqBTK3S9dP
# Y+fSLWLxRT3nrAgA9kahntFbjCZT6HqqSvJGzzc8OJ60d1ylF56NyxGPVjzBrAlf
# A9MCAwEAAaOCAV4wggFaMA8GA1UdEwEB/wQFMAMBAf8wHQYDVR0OBBYEFMsR6MrS
# tBZYAck3LjMWFrlMmgofMAsGA1UdDwQEAwIBhjASBgkrBgEEAYI3FQEEBQIDAQAB
# MCMGCSsGAQQBgjcVAgQWBBT90TFO0yaKleGYYDuoMW+mPLzYLTAZBgkrBgEEAYI3
# FAIEDB4KAFMAdQBiAEMAQTAfBgNVHSMEGDAWgBQOrIJgQFYnl+UlE/wq4QpTlVnk
# pDBQBgNVHR8ESTBHMEWgQ6BBhj9odHRwOi8vY3JsLm1pY3Jvc29mdC5jb20vcGtp
# L2NybC9wcm9kdWN0cy9taWNyb3NvZnRyb290Y2VydC5jcmwwVAYIKwYBBQUHAQEE
# SDBGMEQGCCsGAQUFBzAChjhodHRwOi8vd3d3Lm1pY3Jvc29mdC5jb20vcGtpL2Nl
# cnRzL01pY3Jvc29mdFJvb3RDZXJ0LmNydDANBgkqhkiG9w0BAQUFAAOCAgEAWTk+
# fyZGr+tvQLEytWrrDi9uqEn361917Uw7LddDrQv+y+ktMaMjzHxQmIAhXaw9L0y6
# oqhWnONwu7i0+Hm1SXL3PupBf8rhDBdpy6WcIC36C1DEVs0t40rSvHDnqA2iA6VW
# 4LiKS1fylUKc8fPv7uOGHzQ8uFaa8FMjhSqkghyT4pQHHfLiTviMocroE6WRTsgb
# 0o9ylSpxbZsa+BzwU9ZnzCL/XB3Nooy9J7J5Y1ZEolHN+emjWFbdmwJFRC9f9Nqu
# 1IIybvyklRPk62nnqaIsvsgrEA5ljpnb9aL6EiYJZTiU8XofSrvR4Vbo0HiWGFzJ
# NRZf3ZMdSY4tvq00RBzuEBUaAF3dNVshzpjHCe6FDoxPbQ4TTj18KUicctHzbMrB
# 7HCjV5JXfZSNoBtIA1r3z6NnCnSlNu0tLxfI5nI3EvRvsTxngvlSso0zFmUeDord
# EN5k9G/ORtTTF+l5xAS00/ss3x+KnqwK+xMnQK3k+eGpf0a7B2BHZWBATrBC7E7t
# s3Z52Ao0CW0cgDEf4g5U3eWh++VHEK1kmP9QFi58vwUheuKVQSdpw5OPlcmN2Jsh
# rg1cnPCiroZogwxqLbt2awAdlq3yFnv2FoMkuYjPaqhHMS+a3ONxPdcAfmJH0c6I
# ybgY+g5yjcGjPa8CQGr/aZuW4hCoELQ3UAjWwz0wggYHMIID76ADAgECAgphFmg0
# AAAAAAAcMA0GCSqGSIb3DQEBBQUAMF8xEzARBgoJkiaJk/IsZAEZFgNjb20xGTAX
# BgoJkiaJk/IsZAEZFgltaWNyb3NvZnQxLTArBgNVBAMTJE1pY3Jvc29mdCBSb290
# IENlcnRpZmljYXRlIEF1dGhvcml0eTAeFw0wNzA0MDMxMjUzMDlaFw0yMTA0MDMx
# MzAzMDlaMHcxCzAJBgNVBAYTAlVTMRMwEQYDVQQIEwpXYXNoaW5ndG9uMRAwDgYD
# VQQHEwdSZWRtb25kMR4wHAYDVQQKExVNaWNyb3NvZnQgQ29ycG9yYXRpb24xITAf
# BgNVBAMTGE1pY3Jvc29mdCBUaW1lLVN0YW1wIFBDQTCCASIwDQYJKoZIhvcNAQEB
# BQADggEPADCCAQoCggEBAJ+hbLHf20iSKnxrLhnhveLjxZlRI1Ctzt0YTiQP7tGn
# 0UytdDAgEesH1VSVFUmUG0KSrphcMCbaAGvoe73siQcP9w4EmPCJzB/LMySHnfL0
# Zxws/HvniB3q506jocEjU8qN+kXPCdBer9CwQgSi+aZsk2fXKNxGU7CG0OUoRi4n
# rIZPVVIM5AMs+2qQkDBuh/NZMJ36ftaXs+ghl3740hPzCLdTbVK0RZCfSABKR2YR
# JylmqJfk0waBSqL5hKcRRxQJgp+E7VV4/gGaHVAIhQAQMEbtt94jRrvELVSfrx54
# QTF3zJvfO4OToWECtR0Nsfz3m7IBziJLVP/5BcPCIAsCAwEAAaOCAaswggGnMA8G
# A1UdEwEB/wQFMAMBAf8wHQYDVR0OBBYEFCM0+NlSRnAK7UD7dvuzK7DDNbMPMAsG
# A1UdDwQEAwIBhjAQBgkrBgEEAYI3FQEEAwIBADCBmAYDVR0jBIGQMIGNgBQOrIJg
# QFYnl+UlE/wq4QpTlVnkpKFjpGEwXzETMBEGCgmSJomT8ixkARkWA2NvbTEZMBcG
# CgmSJomT8ixkARkWCW1pY3Jvc29mdDEtMCsGA1UEAxMkTWljcm9zb2Z0IFJvb3Qg
# Q2VydGlmaWNhdGUgQXV0aG9yaXR5ghB5rRahSqClrUxzWPQHEy5lMFAGA1UdHwRJ
# MEcwRaBDoEGGP2h0dHA6Ly9jcmwubWljcm9zb2Z0LmNvbS9wa2kvY3JsL3Byb2R1
# Y3RzL21pY3Jvc29mdHJvb3RjZXJ0LmNybDBUBggrBgEFBQcBAQRIMEYwRAYIKwYB
# BQUHMAKGOGh0dHA6Ly93d3cubWljcm9zb2Z0LmNvbS9wa2kvY2VydHMvTWljcm9z
# b2Z0Um9vdENlcnQuY3J0MBMGA1UdJQQMMAoGCCsGAQUFBwMIMA0GCSqGSIb3DQEB
# BQUAA4ICAQAQl4rDXANENt3ptK132855UU0BsS50cVttDBOrzr57j7gu1BKijG1i
# uFcCy04gE1CZ3XpA4le7r1iaHOEdAYasu3jyi9DsOwHu4r6PCgXIjUji8FMV3U+r
# kuTnjWrVgMHmlPIGL4UD6ZEqJCJw+/b85HiZLg33B+JwvBhOnY5rCnKVuKE5nGct
# xVEO6mJcPxaYiyA/4gcaMvnMMUp2MT0rcgvI6nA9/4UKE9/CCmGO8Ne4F+tOi3/F
# NSteo7/rvH0LQnvUU3Ih7jDKu3hlXFsBFwoUDtLaFJj1PLlmWLMtL+f5hYbMUVbo
# nXCUbKw5TNT2eb+qGHpiKe+imyk0BncaYsk9Hm0fgvALxyy7z0Oz5fnsfbXjpKh0
# NbhOxXEjEiZ2CzxSjHFaRkMUvLOzsE1nyJ9C/4B5IYCeFTBm6EISXhrIniIh0EPp
# K+m79EjMLNTYMoBMJipIJF9a6lbvpt6Znco6b72BJ3QGEe52Ib+bgsEnVLaxaj2J
# oXZhtG6hE6a/qkfwEm/9ijJssv7fUciMI8lmvZ0dhxJkAj0tr1mPuOQh5bWwymO0
# eFQF1EEuUKyUsKV4q7OglnUa2ZKHE3UiLzKoCG6gW4wlv6DvhMoh1useT8ma7kng
# 9wFlb4kLfchpyOZu6qeXzjEp/w7FW1zYTRuh2Povnj8uVRZryROj/TGCBJcwggST
# AgEBMIGQMHkxCzAJBgNVBAYTAlVTMRMwEQYDVQQIEwpXYXNoaW5ndG9uMRAwDgYD
# VQQHEwdSZWRtb25kMR4wHAYDVQQKExVNaWNyb3NvZnQgQ29ycG9yYXRpb24xIzAh
# BgNVBAMTGk1pY3Jvc29mdCBDb2RlIFNpZ25pbmcgUENBAhMzAAABCix5rtd5e6as
# AAEAAAEKMAkGBSsOAwIaBQCggbAwGQYJKoZIhvcNAQkDMQwGCisGAQQBgjcCAQQw
# HAYKKwYBBAGCNwIBCzEOMAwGCisGAQQBgjcCARUwIwYJKoZIhvcNAQkEMRYEFHHl
# WmwWVWC77+MZbWCHoeSV6zbaMFAGCisGAQQBgjcCAQwxQjBAoBaAFABQAG8AdwBl
# AHIAUwBoAGUAbABsoSaAJGh0dHA6Ly93d3cubWljcm9zb2Z0LmNvbS9Qb3dlclNo
# ZWxsIDANBgkqhkiG9w0BAQEFAASCAQAu7lr/wtQ2YMxp6s63su3OSS/+e1QplQUN
# 3nXfWoqYW/C6FHzqHQFwR/dG+3rXtOlD5xNjvoyoXQfGIud63vtJiHJs/nblcWsL
# NWANUlSXIqTq6OYS4LISRw78e8TZd85XJ65KHhMR7slYYXkeYtC02M88nX+WVA1i
# KQJPyF/onCm9qL7B6eaR8hUVwjDUQiCA2g1Z1gTxtNcKVNW4+8S5mgZ+NF2UwWnJ
# fy7/jlygM8cPnn5cyAqsTbbp2/8nVTM/GbdYuayfzb8BtbwimriiKVT0TH/qTxiM
# h3CKOXr1K7jjymaBcXBk1KO88cENINUNIyt0y4YIdq0bPj2OYifzoYICKDCCAiQG
# CSqGSIb3DQEJBjGCAhUwggIRAgEBMIGOMHcxCzAJBgNVBAYTAlVTMRMwEQYDVQQI
# EwpXYXNoaW5ndG9uMRAwDgYDVQQHEwdSZWRtb25kMR4wHAYDVQQKExVNaWNyb3Nv
# ZnQgQ29ycG9yYXRpb24xITAfBgNVBAMTGE1pY3Jvc29mdCBUaW1lLVN0YW1wIFBD
# QQITMwAAAJb6gDHvN2RGRQAAAAAAljAJBgUrDgMCGgUAoF0wGAYJKoZIhvcNAQkD
# MQsGCSqGSIb3DQEHATAcBgkqhkiG9w0BCQUxDxcNMTYwNDIxMjI1MTM5WjAjBgkq
# hkiG9w0BCQQxFgQUCN8b9EXu29+Rm3QO+EN5ddLIoA0wDQYJKoZIhvcNAQEFBQAE
# ggEAKVQuIWGvXCx9fUSMm+cdO0a7R97P+kqfC72FS5m8PW6phrKfLTHAUBSQBaJx
# z34zOTjBsUUZRqdIVgh/VK50GFLupBg7uvpX5QwP6KEj8Th2DIoI3O6CKxfSpZU6
# jbWFLa0WmEFBnzWvOK2CZwbHnGFmudy07oYGnln+45wx0hX6YcybwgLqermR+ULW
# /wzkepHTs+WZxfh7ZyWi2EmYzeyABj9TuiG5HI4VU9tz7xqbcwJiGSLI/fTIWz4B
# 6DMya2mUzbGPn53ouNkS3t37s8qRoaBHc4SkMCLwhbi8y9I6oZx70jSjRbKu3FnC
# +eY+hq8gj8dEwVWLc+yV0tTZKw==
# SIG # End signature block