BhudModuleLib.ps1

#Requires -Version 7.0

Add-Type -AssemblyName System.IO.Compression

enum TestResult {
    na
    ok
    warn
    fail
}

class BhudUtil {
    static [void] WriteTestStart([String] $test) {
        Write-Host $test.PadRight(32, '.') -NoNewLine
    }

    static [void] WriteTestOk([string] $value) {
        [BhudUtil]::WriteTestResult("OK [$($value)]", [TestResult]::ok)
    }

    static [void] WriteTestResult([String] $details, [TestResult] $result) {
        $foregroundLookup = @{
            [TestResult]::na   = "Blue";
            [TestResult]::ok   = "Green";
            [TestResult]::warn = "Yellow";
            [TestResult]::fail = "Red"
        }

        Write-Host $details -ForegroundColor $foregroundLookup[$result]
    }
}

class BhudModule {
    [int]    $MANIFEST_VERSION = 1
    [String] $MANIFEST_NAME    = "manifest.json"

    [System.IO.Compression.ZipArchive] $Archive

    [string] $Checksum
    
    BhudModule([System.IO.Compression.ZipArchive] $ModuleArchive) {
        $this.MANIFEST_VERSION = 1
        $this.MANIFEST_NAME    = "manifest.json"

        $this.Archive = $ModuleArchive
    }

    [Object] GetManifest() {
        $manifestEntry = $this.Archive.GetEntry($this.MANIFEST_NAME)
        $manifestReader = New-Object System.IO.StreamReader($manifestEntry.Open())

        return ($manifestReader.ReadToEnd() | ConvertFrom-Json -AsHashtable)
    }

    [bool] Validate() {
        $isValid = $true

        # check manifest exists
        [BhudUtil]::WriteTestStart("Checking $($this.MANIFEST_NAME) exists")
        if (!$this.Archive.GetEntry($this.MANIFEST_NAME)) {
            [BhudUtil]::WriteTestResult("$($this.MANIFEST_NAME) is missing from archive!", [TestResult]::fail)
            Write-Host "Unable to continue validation!" -ForegroundColor Red
            return $false; # can't continue validating
        } else {
            [BhudUtil]::WriteTestResult("OK", [TestResult]::ok)
        }

        $manifest = $this.GetManifest();

        # check module name
        [BhudUtil]::WriteTestStart("Checking name")
        if (!$manifest.name) {
            [BhudUtil]::WriteTestResult("Mandatory field 'name' missing from manifest!", [TestResult]::fail)
            $isValid = $false
        } else {
            [BhudUtil]::WriteTestOk($manifest.name)
        }

        # check manifest version
        [BhudUtil]::WriteTestStart("Checking manifest version")
        if (!$manifest.manifest_version) {
            [BhudUtil]::WriteTestResult("Mandatory field 'manifest_version' missing from manifest!", [TestResult]::fail)
            $isValid = $false
        } elseif ($manifest.manifest_version -ne $this.MANIFEST_VERSION) {
            [BhudUtil]::WriteTestResult("Provided version '$($manifest.manifest_version)' is invalid (must be '$($this.MANIFEST_VERSION)').", [TestResult]::fail)
            $isValid = $false
        } else {
            [BhudUtil]::WriteTestOk($manifest.manifest_version)
        }

        # check primary assembly name
        [BhudUtil]::WriteTestStart("Checking package reference")
        if (!$manifest.package) {
            [BhudUtil]::WriteTestResult("Mandatory field 'package' missing from manifest!", [TestResult]::fail)
            $isValid = $false
        } elseif (!$this.Archive.GetEntry($manifest.package)) { 
            [BhudUtil]::WriteTestResult("Provided '$($manifest.package)' does not reference a valid module DLL!", [TestResult]::fail)
            $isValid = $false
        } else {
            [BhudUtil]::WriteTestOk($manifest.package)
        }

        # check module version
        [BhudUtil]::WriteTestStart("Checking version")
        if (!$manifest.version) {
            [BhudUtil]::WriteTestResult("Mandatory field 'version' missing from manifest!", [TestResult]::fail)
            $isValid = $false
        } elseif ($manifest.version -notmatch "^(0|[1-9]\d*)\.(0|[1-9]\d*)\.(0|[1-9]\d*)(?:-((?:0|[1-9]\d*|\d*[a-zA-Z-][0-9a-zA-Z-]*)(?:\.(?:0|[1-9]\d*|\d*[a-zA-Z-][0-9a-zA-Z-]*))*))?(?:\+([0-9a-zA-Z-]+(?:\.[0-9a-zA-Z-]+)*))?$") {
            # https://semver.org/#is-there-a-suggested-regular-expression-regex-to-check-a-semver-string
            [BhudUtil]::WriteTestResult("Provided '$($manifest.version)' is invalid (must be SemVer).", [TestResult]::fail)
            $isValid = $false
        } else {
            [BhudUtil]::WriteTestOk($manifest.version)
        }

        # check namespace
        [BhudUtil]::WriteTestStart("Checking namespace")
        if (!$manifest.namespace) {
            [BhudUtil]::WriteTestResult("Mandatory field 'namespace' missing from manifest!", [TestResult]::fail)
            $isValid = $false
        } elseif ($manifest.namespace -notmatch "^([a-zA-Z_][\w_]*\.)*[a-zA-Z_][\w_]*$") {
            # https://stackoverflow.com/a/5205467
            [BhudUtil]::WriteTestResult("Provided '$($manifest.namespace)' is invalid.", [TestResult]::fail)
            $isValid = $false
        } else {
            [BhudUtil]::WriteTestOk($manifest.namespace)
        }

        ### optional fields

        # api permission check
        [BhudUtil]::WriteTestStart("Checking API permissions")
        if (!$manifest.api_permissions) {
            [BhudUtil]::WriteTestResult("NA", [TestResult]::na)
        } else {
            $refPermissions = @("account", "inventories", "characters", "tradingpost", "wallet", "unlocks", "pvp", "builds", "progression", "guilds")

            $reqPermissions = $($manifest.api_permissions.Keys)
            
            $badPermissions = ((Compare-Object -ReferenceObject $refPermissions -DifferenceObject $reqPermissions) | Where-Object {$_.SideIndicator -eq '=>'}) | Select-Object -ExpandProperty InputObject

            if ($badPermissions.Length -gt 0) {
                [BhudUtil]::WriteTestResult("Invalid permissions [$($badPermissions -join ', ')]", [TestResult]::fail)
                $isValid = $false;
            } else {
                [BhudUtil]::WriteTestOk($reqPermissions -join ', ')
            }
        }

        # directory check
        [BhudUtil]::WriteTestStart("Checking directories")
        if (!$manifest.directories) {
            [BhudUtil]::WriteTestResult("NA", [TestResult]::na)
        } else {
            $badDirs = $manifest.directories -notmatch "^[^<>:""\/\\|\?\*\r\n]+$"

            if ($badDirs.Length -gt 0) {
                [BhudUtil]::WriteTestResult("Invalid dirs [$($badDirs -join ', ')]", [TestResult]::fail)
                $isValid = $false;
            } else {
                [BhudUtil]::WriteTestOk($manifest.directories -join ', ')
            }
        }

        # checking enabled with without Blish HUD
        [BhudUtil]::WriteTestStart("Checking enabled without game")
        if (!$manifest.enable_without_gw2) {
            [BhudUtil]::WriteTestResult("FALSE", [TestResult]::na)
        } else {
            if ($manifest.enable_without_gw2 -eq $true) {
                [BhudUtil]::WriteTestResult("TRUE", [TestResult]::warn)
            } else {
                [BhudUtil]::WriteTestResult("FALSE", [TestResult]::ok)
            }
        }

        return $isValid;
    }
}

function Get-BhudModule {
    [OutputType([BhudModule])]
    param(
        [System.IO.MemoryStream] $InputStream = $null,
        [String] $Path = "",
        [String] $Url = ""
    )

    $moduleStream = New-Object System.IO.MemoryStream

    if ($InputStream) {
        $moduleStream = $InputStream
    }

    if ($Path.Length -gt 0) {
        (Get-Item -Path $Path).OpenRead().CopyTo($moduleStream)
    } elseif ($Url.Length -gt 0) {
        write-host "Downloading module from URL. This could take a while..." -ForeGroundColor Blue -NoNewline
        $webClient = New-Object System.Net.WebClient

        try {
            $webClient.OpenRead($URL).CopyTo($moduleStream)
            Write-Host "OK" -ForegroundColor Green
        } catch {
            write-host "Error downloading module." -ForeGroundColor red
            Write-Host "FAILED" -ForegroundColor Red
            return $null
        }
    }

    $module = [BhudModule]::new($moduleStream)

    # Consider moving to the ctor
    $moduleStream.Position = 0
    $module.Checksum = (Get-FileHash -InputStream $moduleStream).Hash

    return $module
}