Private/Code.ps1

# What a code means.
#
# A status or an error code is never shown to a Technician bare. The code stays - it is
# what gets looked up - and what it means stands beside it. These are the decoders the
# Kinds share: the error numbers of Windows, HTTP statuses, the sign-in statuses of
# Windows and of Microsoft Entra ID. A code none of them knows comes back with nothing,
# and is then shown as it is: that is a gap to close and not a reason to hide the code.
#
# All pure. Where a table here is not Microsoft's own documentation, it says where it is from.

function ConvertTo-CodeNumber {
    <#
    .SYNOPSIS
        A code as a number, however it was written: 1067, '0x5', 'C000006D'. $null for
        what is none. Pure.
    #>

    [CmdletBinding()]
    [OutputType([long])]
    param([AllowNull()]$Code, [switch]$Hex)

    $text = "$Code".Trim()
    if (-not $text) { return $null }
    $number = [long]0
    if ($text -match '^(?i)0x([0-9a-f]+)$') {
        if ([long]::TryParse($Matches[1], [Globalization.NumberStyles]::HexNumber, [cultureinfo]::InvariantCulture, [ref]$number)) { return $number }
        return $null
    }
    if ($Hex -and $text -match '^(?i)[0-9a-f]{8}$') {
        if ([long]::TryParse($text, [Globalization.NumberStyles]::HexNumber, [cultureinfo]::InvariantCulture, [ref]$number)) { return $number }
    }
    if ([long]::TryParse($text, [Globalization.NumberStyles]::Integer, [cultureinfo]::InvariantCulture, [ref]$number)) { return $number }
    $null
}

function Get-Win32ErrorText {
    <#
    .SYNOPSIS
        What Windows itself says of one of its error numbers, in the language Windows
        runs in. Nothing for zero and for a number it has no words for. Pure.
    .DESCRIPTION
        The exit code of a service, the error of a Group Policy run, the low word of an
        HRESULT that begins 0x8007. Windows carries the sentences; a number it does not
        know comes back as "Unknown error (0x...)", which is no meaning and is dropped.
    #>

    [CmdletBinding()]
    [OutputType([string])]
    param([AllowNull()]$Code)

    $number = ConvertTo-CodeNumber -Code $Code
    if ($null -eq $number -or $number -le 0 -or $number -gt [int]::MaxValue) { return '' }
    # A number Windows has no sentence for and the field knows well. 283 is 0x11B: what
    # "Windows cannot connect to the printer, 0x0000011b" shows, since the print spooler
    # update of September 2021 (CVE-2021-1678) made client and print server agree on how
    # the connection is secured. Not a table of Microsoft's.
    if ($number -eq 283) { return Get-Text 'Value.Code.Win32.283' }
    try {
        $message = "$((New-Object System.ComponentModel.Win32Exception ([int]$number)).Message)".Trim()
        if ($message -match '(?i)^(Unknown error|Unbekannter Fehler)' -or $message -match '\(0x[0-9a-f]+\)$') { return '' }
        $message
    }
    catch { '' }
}

function Get-WindowsMessageText {
    <#
    .SYNOPSIS
        The sentence a library of Windows carries for one of its codes, in the language
        Windows runs in. Nothing for a code it has none for. Reads Windows; changes nothing.
    .DESCRIPTION
        ntdll.dll carries one for every status of Windows (NTSTATUS), winhttp.dll for
        every WinHTTP error; Get-Win32ErrorText does not reach either. A sentence with a
        place left open for a value ("%hs") has [...] there; a title in braces
        before it is kept as its beginning; of a long one the first sentence is kept.
    #>

    [CmdletBinding()]
    [OutputType([string])]
    param([Parameter(Mandatory)][ValidateSet('ntdll.dll', 'winhttp.dll')][string]$Library, [AllowNull()]$Id)

    if ($null -eq $Id) { return '' }
    try {
        if (-not ('Gutcheck.WindowsMessage' -as [type])) {
            Add-Type -Namespace Gutcheck -Name WindowsMessage -MemberDefinition @'
[DllImport("kernel32.dll", CharSet = CharSet.Unicode, SetLastError = true)]
public static extern IntPtr LoadLibraryEx(string file, IntPtr reserved, uint flags);
[DllImport("kernel32.dll")]
public static extern bool FreeLibrary(IntPtr module);
[DllImport("kernel32.dll", CharSet = CharSet.Unicode, SetLastError = true)]
public static extern int FormatMessage(uint flags, IntPtr source, uint id, uint language, System.Text.StringBuilder buffer, int size, IntPtr arguments);
'@

        }
        $number = [uint32]([long]$Id -band 0xFFFFFFFFL)
        # As a data file only: nothing of the library is run.
        $module = [Gutcheck.WindowsMessage]::LoadLibraryEx($Library, [IntPtr]::Zero, 0x2)
        if ($module -eq [IntPtr]::Zero) { return '' }
        try {
            $buffer = New-Object System.Text.StringBuilder 2048
            # From the module, inserts left as they are.
            $length = [Gutcheck.WindowsMessage]::FormatMessage(0x800 -bor 0x200, $module, $number, 0, $buffer, $buffer.Capacity, [IntPtr]::Zero)
            if ($length -le 0) { return '' }
            $text = $buffer.ToString()
        }
        finally { [void][Gutcheck.WindowsMessage]::FreeLibrary($module) }

        # Line by line: a title in braces becomes the beginning, and a line that ends
        # without a full stop gets one, so that the lines do not run into each other.
        $lines = @($text -split '\r?\n' | ForEach-Object { $_.Trim() } | Where-Object { $_ } | ForEach-Object {
            $line = $_ -replace '^\{([^}]*)\}$', '$1:' -replace '^\{([^}]*)\}\s*', '$1: '
            if ($line -notmatch '[.:!?,;]$') { $line + '.' } else { $line }
        })
        $text = $lines -join ' '
        # A place left open for a value: [...] stands where the value would.
        $text = $text -replace '"?(0x)?%[0-9]*!?[a-zA-Z]+!?"?', '[...]' -replace '%[0-9]+', '[...]'
        $text = ($text -replace '\s+', ' ').Trim()
        if ($text.Length -gt 160) {
            $end = $text.IndexOf('. ')
            if ($end -gt 20) { $text = $text.Substring(0, $end + 1) }
        }
        $text.TrimEnd('.')
    }
    catch { '' }
}

function Get-HttpStatusText {
    <#
    .SYNOPSIS
        What an HTTP status means, in a Technician's words: the status's own where this has
        them, else what its class means. Nothing for what is no HTTP status. Pure.
    .DESCRIPTION
        0 is no answer at all: a client writes it where the connection failed before the
        server said anything. -Exact asks for a status's own words only, and not its
        class's: Format-HttpStatus puts the reason a server gave before the class.
    #>

    [CmdletBinding()]
    [OutputType([string])]
    param([AllowNull()]$Status, [switch]$Exact)

    $number = ConvertTo-CodeNumber -Code $Status
    if ($null -eq $number) { return '' }
    switch ([int]$number) {
        0   { return Get-Text 'Value.Code.Http.0' }
        200 { return Get-Text 'Value.Code.Http.200' }
        204 { return Get-Text 'Value.Code.Http.204' }
        301 { return Get-Text 'Value.Code.Http.301' }
        302 { return Get-Text 'Value.Code.Http.302' }
        307 { return Get-Text 'Value.Code.Http.302' }
        400 { return Get-Text 'Value.Code.Http.400' }
        401 { return Get-Text 'Value.Code.Http.401' }
        403 { return Get-Text 'Value.Code.Http.403' }
        404 { return Get-Text 'Value.Code.Http.404' }
        407 { return Get-Text 'Value.Code.Http.407' }
        408 { return Get-Text 'Value.Code.Http.408' }
        429 { return Get-Text 'Value.Code.Http.429' }
        500 { return Get-Text 'Value.Code.Http.500' }
        502 { return Get-Text 'Value.Code.Http.502' }
        503 { return Get-Text 'Value.Code.Http.503' }
        504 { return Get-Text 'Value.Code.Http.504' }
    }
    if ($Exact) { return '' }
    if ($number -ge 100 -and $number -lt 200) { return Get-Text 'Value.Code.Http.1xx' }
    if ($number -ge 200 -and $number -lt 300) { return Get-Text 'Value.Code.Http.2xx' }
    if ($number -ge 300 -and $number -lt 400) { return Get-Text 'Value.Code.Http.3xx' }
    if ($number -ge 400 -and $number -lt 500) { return Get-Text 'Value.Code.Http.4xx' }
    if ($number -ge 500 -and $number -lt 600) { return Get-Text 'Value.Code.Http.5xx' }
    ''
}

function Format-CodeWithMeaning {
    <#
    .SYNOPSIS
        A code with what it means behind it; the code alone where nothing is known. Pure.
    #>

    [CmdletBinding()]
    [OutputType([string])]
    param([AllowNull()][AllowEmptyString()][string]$Code, [AllowNull()][AllowEmptyString()][string]$Meaning)

    if (-not "$Code".Trim()) { return '' }
    if (-not "$Meaning".Trim()) { return "$Code".Trim() }
    (Get-Text 'Value.Code.WithMeaning') -f "$Code".Trim(), "$Meaning".Trim()
}

function Format-HttpStatus {
    <#
    .SYNOPSIS
        An HTTP status with what it means behind it, "404 (nicht gefunden)": never the
        number alone. Nothing for no status. Pure.
    .DESCRIPTION
        The one way every Kind says a status. Its own words where this has them; else the
        reason the server gave with it (-Reason), "423 (Locked)"; else what its class
        means, "418 (Fehler der Anfrage)".
    #>

    [CmdletBinding()]
    [OutputType([string])]
    param([AllowNull()]$Status, [AllowNull()][AllowEmptyString()][string]$Reason)

    $number = ConvertTo-CodeNumber -Code $Status
    if ($null -eq $number) { return (Format-CodeWithMeaning -Code "$Status" -Meaning '') }
    $meaning = Get-HttpStatusText -Status $number -Exact
    if (-not $meaning) { $meaning = "$Reason".Trim() }
    if (-not $meaning) { $meaning = Get-HttpStatusText -Status $number }
    Format-CodeWithMeaning -Code "$number" -Meaning $meaning
}

function Get-NtStatusText {
    <#
    .SYNOPSIS
        What a sign-in status of Windows (NTSTATUS) means. Nothing for one not known here. Pure.
    .DESCRIPTION
        The ones a sign-in ends in, as Microsoft documents them for logon events
        (event 4625, "Status" and "Sub Status"), and the ones Microsoft's article on
        troubleshooting the primary refresh token names for the "Attempt Status" of
        dsregcmd /status: the network was not there, or the logon session was gone.
        Any other status is said as Windows says it, unless -KnownHere asks for the
        ones worded here only.
    #>

    [CmdletBinding()]
    [OutputType([string])]
    param([AllowNull()]$Code, [switch]$KnownHere)

    $number = ConvertTo-CodeNumber -Code $Code -Hex
    if ($null -eq $number) { return '' }
    switch ('{0:X8}' -f ($number -band 0xFFFFFFFFL)) {
        'C000006D' { return Get-Text 'Value.Code.NtStatus.LogonFailure' }
        'C000006A' { return Get-Text 'Value.Code.NtStatus.WrongPassword' }
        'C0000064' { return Get-Text 'Value.Code.NtStatus.NoSuchUser' }
        'C0000234' { return Get-Text 'Value.Code.NtStatus.LockedOut' }
        'C0000072' { return Get-Text 'Value.Code.NtStatus.Disabled' }
        'C0000071' { return Get-Text 'Value.Code.NtStatus.PasswordExpired' }
        'C0000224' { return Get-Text 'Value.Code.NtStatus.PasswordMustChange' }
        'C0000193' { return Get-Text 'Value.Code.NtStatus.AccountExpired' }
        'C000006F' { return Get-Text 'Value.Code.NtStatus.LogonHours' }
        'C0000070' { return Get-Text 'Value.Code.NtStatus.Workstation' }
        'C000005E' { return Get-Text 'Value.Code.NtStatus.NoLogonServers' }
        'C0000133' { return Get-Text 'Value.Code.NtStatus.TimeDifference' }
        'C000015B' { return Get-Text 'Value.Code.NtStatus.LogonType' }
        'C000005F' { return Get-Text 'Value.Code.NtStatus.NoLogonSession' }
        'C00000BE' { return Get-Text 'Value.Code.NtStatus.BadNetworkPath' }
        'C00000C4' { return Get-Text 'Value.Code.NtStatus.UnexpectedNetworkError' }
        'C00000D0' { return Get-Text 'Value.Code.NtStatus.RequestNotAccepted' }
        'C000023C' { return Get-Text 'Value.Code.NtStatus.NetworkUnreachable' }
        # What a share is refused with (SMBClient 31010): Windows' own sentence is three
        # lines for two words.
        'C0000022' { return Get-Text 'Value.Code.NtStatus.AccessDenied' }
        'C00000CC' { return Get-Text 'Value.Code.NtStatus.BadNetworkName' }
    }
    if ($KnownHere) { return '' }
    # Not one of those: Windows has a sentence for every status it knows.
    Get-WindowsMessageText -Library 'ntdll.dll' -Id $number
}

function Get-HResultText {
    <#
    .SYNOPSIS
        What an HRESULT means: a Win32 error it wraps, a sign-in status of Windows, or
        one of the sign-in broker's own. Nothing for one not known here. Pure.
    .DESCRIPTION
        0x8007nnnn is the Win32 error nnnn. 0xCAAnnnnn are the codes of the Microsoft
        account and work account broker (AAD/WAM) that Office logs; the ones named here
        are those Microsoft's troubleshooting articles for Office sign-in name, and
        0xCAA82EEn carry a WinHTTP error in their low word (12002 timeout, 12007 name not
        resolved, 12029 cannot connect, 12030 connection aborted, 12175 no secure
        connection). The same WinHTTP errors wrapped as 0x80072nnn are named here too:
        Windows keeps their sentences in winhttp.dll and not where Get-Win32ErrorText
        reads, so it has none for them. The errors of AppX deployment (0x80073CFn) need no
        table: they are Win32 errors Windows has words for.
    #>

    [CmdletBinding()]
    [OutputType([string])]
    param([AllowNull()]$Code)

    $number = ConvertTo-CodeNumber -Code $Code -Hex
    if ($null -eq $number) { return '' }
    $hex = '{0:X8}' -f ($number -band 0xFFFFFFFFL)

    switch ($hex) {
        '80072EE2' { return Get-Text 'Value.Code.WinHttp.Timeout' }
        '80072EE7' { return Get-Text 'Value.Code.WinHttp.NameNotResolved' }
        '80072EFD' { return Get-Text 'Value.Code.WinHttp.CannotConnect' }
        '80072EFE' { return Get-Text 'Value.Code.WinHttp.ConnectionAborted' }
        '80072F8F' { return Get-Text 'Value.Code.WinHttp.SecureFailure' }
        'CAA82EFE' { return Get-Text 'Value.Code.WinHttp.ConnectionAborted' }
        'CAA82F8F' { return Get-Text 'Value.Code.WinHttp.SecureFailure' }
        # Two errors of the directory (LDAP), said with the LDAP error the ADSI LDAP
        # provider maps to each, which Windows' own sentence leaves out. As Microsoft
        # words them: 8250 ERROR_DS_SERVER_DOWN "The server is not operational.", from
        # LDAP_SERVER_DOWN "Cannot contact the LDAP server."; 8224
        # ERROR_DS_OPERATIONS_ERROR "An operations error occurred.", from
        # LDAP_OPERATIONS_ERROR, which says no more. Sources: "System Error Codes
        # (8200-8999)", https://learn.microsoft.com/windows/win32/debug/system-error-codes--8200-8999-,
        # and "Win32 Error Codes" (ADSI), https://learn.microsoft.com/windows/win32/adsi/win32-error-codes.
        '8007203A' { return Get-Text 'Value.Code.Ldap.ServerDown' }
        '80072020' { return Get-Text 'Value.Code.Ldap.OperationsError' }
        # ADSI's own, facility 5 (0x80005nnn, E_ADS_*): Windows has no sentence for them,
        # and ADSI writes "Unknown error (0x80005000)". 0x80005000 is E_ADS_BAD_PATHNAME,
        # "An invalid ADSI pathname was passed.", for which Microsoft's corrective action
        # is to check that the object exists on the directory server and the path has no
        # typing error. Source: "Generic ADSI Error Codes",
        # https://learn.microsoft.com/windows/win32/adsi/generic-adsi-error-codes.
        '80005000' { return Get-Text 'Value.Code.Adsi.BadPathname' }
    }
    if ($hex.StartsWith('80005')) { return Get-Text 'Value.Code.Adsi.Other' }
    # Any other WinHTTP error (12000 to 12999), wrapped either way: winhttp.dll has it.
    if ($hex -match '^(8007|CAA8)(2[EF][0-9A-F]{2}|3[0-2][0-9A-F]{2})$') {
        $low = $number -band 0xFFFF
        if ($low -ge 12000 -and $low -le 12999) {
            $said = Get-WindowsMessageText -Library 'winhttp.dll' -Id $low
            if ($said) { return $said }
        }
    }
    if ($hex.StartsWith('8007')) { return Get-Win32ErrorText -Code ($number -band 0xFFFF) }
    switch ($hex) {
        # These three as the event of the Windows sign-in log words them beside the code
        # (read off Microsoft-Windows-AAD/Operational 1098 on a Windows 11 machine):
        # "A login hint was sent that doesn't match any WebAccount in the system",
        # "Token broker operation failed", and invalid_request.
        'CAA100D8' { return Get-Text 'Value.Code.Wam.LoginHint' }
        'CAA5001C' { return Get-Text 'Value.Code.Wam.BrokerFailed' }
        'CAA20002' { return Get-Text 'Value.Code.Wam.InvalidRequest' }
        'CAA20003' { return Get-Text 'Value.Code.Wam.InvalidGrant' }
        'CAA2000C' { return Get-Text 'Value.Code.Wam.InteractionRequired' }
        'CAA70004' { return Get-Text 'Value.Code.Wam.ServerNotFound' }
        'CAA70007' { return Get-Text 'Value.Code.Wam.ServerNotResponding' }
        'CAA82EE2' { return Get-Text 'Value.Code.Wam.Timeout' }
        'CAA82EE7' { return Get-Text 'Value.Code.Wam.NameNotResolved' }
        'CAA82EFD' { return Get-Text 'Value.Code.Wam.CannotConnect' }
    }
    switch ($hex) {
        # OneDrive's own sign-in errors, as Microsoft's troubleshooting articles for each
        # describe them (docs/research/onedrive-sign-in.md): 0x8004DE40 and 0x8004DE88
        # "having trouble connecting to the cloud", 0x8004DE80 and 0x8004DE86 a problem of
        # the account or its configuration, 0x8004DEC5 "multiple cached identities",
        # 0x8004DEB4 after an Office update, worked around by turning on modern sign-in.
        '8004DE40' { return Get-Text 'Value.Code.OneDrive.CannotConnect' }
        '8004DE88' { return Get-Text 'Value.Code.OneDrive.CannotConnect' }
        '8004DE80' { return Get-Text 'Value.Code.OneDrive.Account' }
        '8004DE86' { return Get-Text 'Value.Code.OneDrive.Account' }
        '8004DEC5' { return Get-Text 'Value.Code.OneDrive.Identities' }
        '8004DEB4' { return Get-Text 'Value.Code.OneDrive.ModernAuth' }
    }
    # Another of OneDrive's: whose code it is. Every code those articles name is 0x8004DEnn.
    if ($hex.StartsWith('8004DE')) { return Get-Text 'Value.Code.OneDrive.Other' }
    # One of the broker's own that is not named above: at least whose code it is. A
    # Technician who knows that it is the sign-in, and not the network or the program,
    # knows where to look and what to search for.
    if ($hex.StartsWith('CAA')) { return Get-Text 'Value.Code.Wam.Other' }
    # An HRESULT of another facility can share its number with no status of Windows,
    # and must not borrow one's sentence: only a plain status (0xC000....) is asked for.
    if ($hex.StartsWith('C000')) { return Get-NtStatusText -Code $number }
    Get-NtStatusText -Code $number -KnownHere
}

function Get-BitsHttpStatus {
    <#
    .SYNOPSIS
        The HTTP status a BITS result carries - 401 of 0x80190191 - or nothing for any other
        result. Pure.
    .DESCRIPTION
        BG_E_HTTP_ERROR_xxx is 0x8019nnnn with the status in its low word. The one rule
        Format-BitsResult says a result by and a Judge marks a refusal by.
    #>

    [CmdletBinding()]
    [OutputType([int])]
    param([AllowNull()]$Code)

    $number = ConvertTo-CodeNumber -Code $Code -Hex
    if ($null -eq $number) { return }
    if (('{0:X8}' -f ($number -band 0xFFFFFFFFL)).StartsWith('8019')) {
        $status = [int]($number -band 0xFFFF)
        if ($status -ge 100 -and $status -le 599) { return $status }
    }
}

function Format-BitsResult {
    <#
    .SYNOPSIS
        The result of a BITS transfer with what it means: the HTTP status it carries, a
        reason of BITS's own, or any other HRESULT as Get-HResultText reads it. Nothing for
        success. Pure.
    .DESCRIPTION
        From Microsoft's BITS Return Values. BG_E_HTTP_ERROR_xxx is 0x8019nnnn with the
        HTTP status in its low word, 0x80190191 for 401 and 0x801901F7 for 503: said as the
        status, as Format-HttpStatus says every status, "0x80190191: HTTP-Status 401
        (Anmeldung verlangt)". The reasons of BITS (0x8020nnnn) named here are those a
        transfer ends in on the network; another one at least says whose it is. A transfer
        also ends in a WinHTTP or a Win32 error, 0x80072EE7 for a name that did not
        resolve, which Get-HResultText knows.
 
        The code is written in hexadecimal, as it is looked up, however it came: the event
        carries 0x80190191 as 2149122449. What is no number is given back as it is.
    #>

    [CmdletBinding()]
    [OutputType([string])]
    param([AllowNull()]$Code)

    $number = ConvertTo-CodeNumber -Code $Code -Hex
    if ($null -eq $number) { return "$Code".Trim() }
    if ($number -eq 0) { return '' }
    $hex = '{0:X8}' -f ($number -band 0xFFFFFFFFL)
    $shown = '0x' + $hex

    $status = Get-BitsHttpStatus -Code $number
    if ($null -ne $status) { return ((Get-Text 'Value.Code.Bits.Http') -f $shown, (Format-HttpStatus -Status $status)) }
    Format-CodeWithMeaning -Code $shown -Meaning (Get-BitsResultText -Code $number)
}

function Get-BitsResultText {
    <#
    .SYNOPSIS
        What a result of a BITS transfer that carries no HTTP status means: a reason of
        BITS's own, else any other HRESULT as Get-HResultText reads it. Nothing where
        nothing is known. Pure.
    #>

    [CmdletBinding()]
    [OutputType([string])]
    param([AllowNull()]$Code)

    $number = ConvertTo-CodeNumber -Code $Code -Hex
    if ($null -eq $number -or $number -eq 0) { return '' }
    $hex = '{0:X8}' -f ($number -band 0xFFFFFFFFL)
    $meaning = switch ($hex) {
        '80200010' { Get-Text 'Value.Code.Bits.NetworkDisconnected' }
        '80200011' { Get-Text 'Value.Code.Bits.MissingFileSize' }
        '80200012' { Get-Text 'Value.Code.Bits.HttpVersion' }
        '80200013' { Get-Text 'Value.Code.Bits.Range' }
    }
    if (-not $meaning -and $hex.StartsWith('8020')) { $meaning = Get-Text 'Value.Code.Bits.Other' }
    if (-not $meaning) { $meaning = Get-HResultText -Code $number }
    "$meaning"
}

function Get-AadStsText {
    <#
    .SYNOPSIS
        What an error number of Microsoft Entra ID (AADSTS) means. Nothing for one not
        known here. Pure.
    .DESCRIPTION
        The ones a user's sign-in ends in, as Microsoft documents them under "Microsoft
        Entra authentication and authorization error codes". 70044 is not in that table:
        it is the session's counterpart of 70043, as the sign-in itself words it ("The
        session has expired or is invalid due to sign-in frequency checks by Conditional
        Access").
    #>

    [CmdletBinding()]
    [OutputType([string])]
    param([AllowNull()]$Code)

    $number = ConvertTo-CodeNumber -Code ("$Code" -replace '(?i)^\s*AADSTS', '')
    if ($null -eq $number) { return '' }
    switch ([long]$number) {
        50020  { return Get-Text 'Value.Code.Aad.50020' }
        50034  { return Get-Text 'Value.Code.Aad.50034' }
        50053  { return Get-Text 'Value.Code.Aad.50053' }
        50055  { return Get-Text 'Value.Code.Aad.50055' }
        50057  { return Get-Text 'Value.Code.Aad.50057' }
        50058  { return Get-Text 'Value.Code.Aad.50058' }
        50074  { return Get-Text 'Value.Code.Aad.50074' }
        50076  { return Get-Text 'Value.Code.Aad.50076' }
        50079  { return Get-Text 'Value.Code.Aad.50079' }
        50097  { return Get-Text 'Value.Code.Aad.50097' }
        50105  { return Get-Text 'Value.Code.Aad.50105' }
        50126  { return Get-Text 'Value.Code.Aad.50126' }
        50133  { return Get-Text 'Value.Code.Aad.50133' }
        50155  { return Get-Text 'Value.Code.Aad.50155' }
        50158  { return Get-Text 'Value.Code.Aad.50158' }
        50173  { return Get-Text 'Value.Code.Aad.50173' }
        53000  { return Get-Text 'Value.Code.Aad.53000' }
        53001  { return Get-Text 'Value.Code.Aad.53001' }
        53003  { return Get-Text 'Value.Code.Aad.53003' }
        65001  { return Get-Text 'Value.Code.Aad.65001' }
        70008  { return Get-Text 'Value.Code.Aad.70008' }
        70043  { return Get-Text 'Value.Code.Aad.70043' }
        70044  { return Get-Text 'Value.Code.Aad.70043' }
        135011 { return Get-Text 'Value.Code.Aad.135011' }
        700016 { return Get-Text 'Value.Code.Aad.700016' }
        700082 { return Get-Text 'Value.Code.Aad.700082' }
    }
    ''
}

function Get-OAuthErrorText {
    <#
    .SYNOPSIS
        What an OAuth error word of Microsoft Entra ID means (invalid_grant and the like).
        Nothing for one not known here. Pure.
    .DESCRIPTION
        The words the token endpoint answers with, as Microsoft documents them for the
        OAuth 2.0 authorization code flow ("Error codes for token endpoint errors"). The
        AAD log and dsregcmd /status carry them beside the AADSTS number, which says more.
    #>

    [CmdletBinding()]
    [OutputType([string])]
    param([AllowNull()][AllowEmptyString()][string]$Code)

    switch ("$Code".Trim().ToLowerInvariant()) {
        'invalid_grant'           { return Get-Text 'Value.Code.Wam.InvalidGrant' }
        'interaction_required'    { return Get-Text 'Value.Code.Wam.InteractionRequired' }
        'consent_required'        { return Get-Text 'Value.Code.Aad.65001' }
        'invalid_client'          { return Get-Text 'Value.Code.OAuth.InvalidClient' }
        'unauthorized_client'     { return Get-Text 'Value.Code.OAuth.UnauthorizedClient' }
        'temporarily_unavailable' { return Get-Text 'Value.Code.OAuth.TemporarilyUnavailable' }
    }
    ''
}

function Get-MicrosoftAppText {
    <#
    .SYNOPSIS
        Which program an application id of Microsoft's is, as a Technician knows it. One
        it does not know is said to be another, with the id; none is said to be none. Pure.
    .DESCRIPTION
        Every program that signs in to Microsoft 365 does so as an application with an
        id of its own, and the sign-in log of Windows names that id with each error. The
        ids here are the ones that sign in on a Windows PC, as Microsoft's own sign-in
        documentation and the directory itself name them (collected in the public list
        merill/microsoft-info, MicrosoftApps.csv, read 2026-10-10; the ones marked there
        as contributed by the community are Edge, the Office app from the Store, the
        company portal, the accounts control and the device management client).
 
        Outlook as part of Office has no id of its own: it signs in as "Microsoft
        Office", as Word and Excel do. Which of them it was the log does not say.
    #>

    [CmdletBinding()]
    [OutputType([string])]
    param([AllowNull()][AllowEmptyString()][string]$ClientId)

    $id = "$ClientId".Trim().ToLowerInvariant()
    if (-not $id) { return Get-Text 'Value.Code.App.NotNamed' }
    switch ($id) {
        'd3590ed6-52b3-4102-aeff-aad2292ab01c' { return Get-Text 'Value.Code.App.Office' }
        '5d661950-3475-41cd-a2c3-d671a3162bc1' { return Get-Text 'Value.Code.App.Outlook' }
        '27922004-5251-4030-b22d-91ecd9a37ea4' { return Get-Text 'Value.Code.App.OutlookMobile' }
        'ab9b8c07-8f02-4f72-87fa-80105867a763' { return Get-Text 'Value.Code.App.OneDrive' }
        '6a9b9266-8161-4a7b-913a-a9eda19da220' { return Get-Text 'Value.Code.App.OneDrive' }
        'b26aadf8-566f-4478-926f-589f601d9c74' { return Get-Text 'Value.Code.App.OneDrive' }
        'ecd6b820-32c2-49b6-98a6-444530e5a77a' { return Get-Text 'Value.Code.App.Edge' }
        'd7b530a4-7680-4c23-a8bf-c52c121d2e87' { return Get-Text 'Value.Code.App.EdgeNewTab' }
        '1fec8e78-bce4-4aaf-ab1b-5451cc387264' { return Get-Text 'Value.Code.App.Teams' }
        '5e3ce6c0-2b1f-4285-8d4b-75ee78787346' { return Get-Text 'Value.Code.App.TeamsWeb' }
        '29d9ed98-a469-4536-ade2-f981bc1d605e' { return Get-Text 'Value.Code.App.Broker' }
        'a40d7d7d-59aa-447e-a655-679a4107e548' { return Get-Text 'Value.Code.App.AccountsControl' }
        '38aa3b87-a06d-4817-b275-7a316988d93b' { return Get-Text 'Value.Code.App.Hello' }
        '26a7ee05-5602-4d76-a7ba-eae8b7b67941' { return Get-Text 'Value.Code.App.Search' }
        '1b3c667f-cde3-4090-b60b-3d2abd0117f0' { return Get-Text 'Value.Code.App.Spotlight' }
        '4765445b-32c6-49b0-83e6-1d93765276ca' { return Get-Text 'Value.Code.App.OfficeHome' }
        '0ec893e0-5785-4de6-99da-4ed124e5296c' { return Get-Text 'Value.Code.App.OfficeStore' }
        'c0ab8ce9-e9a0-42e7-b064-33d422df41f1' { return Get-Text 'Value.Code.App.Copilot' }
        'cf36b471-5b44-428c-9ce7-313bf84528de' { return Get-Text 'Value.Code.App.Bing' }
        '9ba1a5c7-f17a-4de9-a1f1-6178c8d51223' { return Get-Text 'Value.Code.App.CompanyPortal' }
        'fc0f3af4-6835-4174-b806-f7db311fd2f3' { return Get-Text 'Value.Code.App.IntuneAgent' }
        'de50c81f-5f80-4771-b66b-cebd28ccdfc1' { return Get-Text 'Value.Code.App.DeviceManagement' }
    }
    (Get-Text 'Value.Code.App.Other') -f $id
}

function Get-RasErrorText {
    <#
    .SYNOPSIS
        What Windows itself says of an error number of its dial-up and VPN networking, in
        the language Windows runs in. Nothing for a number it has no words for. Reads
        Windows; changes nothing.
    .DESCRIPTION
        The numbers 600 to 999 are not the error numbers of Windows: 809 there is about
        file metadata, and here it is a VPN server that does not answer. Windows keeps
        their sentences apart (RasGetErrorString). A VPN also fails with numbers outside
        that range - 13801, an IKE error - and those are Windows' own. Of a long answer
        the first sentence is kept.
    #>

    [CmdletBinding()]
    [OutputType([string])]
    param([AllowNull()]$Code)

    $number = ConvertTo-CodeNumber -Code $Code
    if ($null -eq $number -or $number -le 0 -or $number -gt [int]::MaxValue) { return '' }
    if ($number -lt 600 -or $number -gt 999) { return (Get-Win32ErrorText -Code $number) }
    try {
        if (-not ('Gutcheck.RasMessage' -as [type])) {
            Add-Type -Namespace Gutcheck -Name RasMessage -MemberDefinition @"
[DllImport("rasapi32.dll", CharSet = CharSet.Unicode)]
public static extern uint RasGetErrorString(uint id, System.Text.StringBuilder buffer, uint size);
"@

        }
        $buffer = New-Object System.Text.StringBuilder 1024
        if ([Gutcheck.RasMessage]::RasGetErrorString([uint32]$number, $buffer, [uint32]$buffer.Capacity) -ne 0) { return '' }
        $text = ($buffer.ToString() -replace '\s+', ' ').Trim()
        if ($text.Length -gt 160) {
            $end = $text.IndexOf('. ')
            if ($end -gt 20) { $text = $text.Substring(0, $end + 1) }
        }
        $text.TrimEnd('.')
    }
    catch { '' }
}

function Get-TlsAlertText {
    <#
    .SYNOPSIS
        What a TLS alert means, by its number. Nothing for a number not in the standard's
        list. Pure.
    .DESCRIPTION
        Schannel logs "a fatal alert was received: 40" and leaves the forty to the
        reader. The numbers are those of the TLS standard (RFC 8446, section 6, and the
        IANA registry of TLS alerts), the same in every version of TLS.
    #>

    [CmdletBinding()]
    [OutputType([string])]
    param([AllowNull()]$Alert)

    $number = ConvertTo-CodeNumber -Code $Alert
    if ($null -eq $number) { return '' }
    switch ([int]$number) {
        10  { return (Get-Text 'Value.Code.TlsAlert.10') }
        20  { return (Get-Text 'Value.Code.TlsAlert.20') }
        40  { return (Get-Text 'Value.Code.TlsAlert.40') }
        42  { return (Get-Text 'Value.Code.TlsAlert.42') }
        43  { return (Get-Text 'Value.Code.TlsAlert.43') }
        44  { return (Get-Text 'Value.Code.TlsAlert.44') }
        45  { return (Get-Text 'Value.Code.TlsAlert.45') }
        46  { return (Get-Text 'Value.Code.TlsAlert.46') }
        47  { return (Get-Text 'Value.Code.TlsAlert.47') }
        48  { return (Get-Text 'Value.Code.TlsAlert.48') }
        49  { return (Get-Text 'Value.Code.TlsAlert.49') }
        50  { return (Get-Text 'Value.Code.TlsAlert.50') }
        51  { return (Get-Text 'Value.Code.TlsAlert.51') }
        70  { return (Get-Text 'Value.Code.TlsAlert.70') }
        71  { return (Get-Text 'Value.Code.TlsAlert.71') }
        80  { return (Get-Text 'Value.Code.TlsAlert.80') }
        86  { return (Get-Text 'Value.Code.TlsAlert.86') }
        90  { return (Get-Text 'Value.Code.TlsAlert.90') }
        112 { return (Get-Text 'Value.Code.TlsAlert.112') }
        116 { return (Get-Text 'Value.Code.TlsAlert.116') }
        120 { return (Get-Text 'Value.Code.TlsAlert.120') }
    }
    ''
}

function Format-CodeInText {
    <#
    .SYNOPSIS
        A text as an event wrote it, with every error code in it ("0x80200011") followed by
        what it means, as Format-BitsResult says it. Pure.
    .DESCRIPTION
        The message of an event is shown as Windows wrote it, and Windows writes the code
        alone: "... mit folgendem Fehler: 0x80200011". Only a code shaped as an error is
        read - an HRESULT or a status of Windows, whose high bit is set: 0x8..., 0xC...,
        0xD... - and only where its meaning is known: the time stamp, the offset and the
        address of a crash are eight hex digits as well, and 0x00000000 is success. The code
        stays as it was written; one already followed by the very meaning it would get is
        left as it is.
    #>

    [CmdletBinding()]
    [OutputType([string])]
    param([AllowNull()][AllowEmptyString()][string]$Text)

    if (-not $Text) { return "$Text" }
    [regex]::Replace($Text, '(?<![0-9A-Za-z])0x[89CDcd][0-9A-Fa-f]{7}(?![0-9A-Za-z])', {
        param($Match)
        $code = $Match.Value
        $status = Get-BitsHttpStatus -Code $code
        $said = $(if ($null -ne $status) { (Get-Text 'Value.Code.Bits.Http') -f $code, (Format-HttpStatus -Status $status) }
                  else { Format-CodeWithMeaning -Code $code -Meaning (Get-BitsResultText -Code $code) })
        # Said already: what follows the code is what would be added.
        $after = $Text.Substring($Match.Index)
        if ($after.StartsWith($said, [StringComparison]::OrdinalIgnoreCase)) { return $code }
        $said
    })
}

function Get-DotNetExceptionText {
    <#
    .SYNOPSIS
        What a .NET exception type means, by its name with or without its namespace:
        "EndpointNotFoundException" or "System.ServiceModel.EndpointNotFoundException".
        ".NET-Ausnahme" for one not known here; nothing for what is no exception's name.
        Pure.
    .DESCRIPTION
        An application's own log writes the type as the error of a line, and the type
        alone is a code: a Technician reads "EndpointNotFoundException" and learns no
        more than that something went wrong. The ones named here are those of the network,
        the files and the common programming faults, which say where to look.
    #>

    [CmdletBinding()]
    [OutputType([string])]
    param([AllowNull()][AllowEmptyString()][string]$Name)

    $short = @("$Name".Trim() -split '[.+]')[-1]
    if ($short -notmatch '^\w+Exception$') { return '' }
    switch ($short) {
        'EndpointNotFoundException'           { return Get-Text 'Value.Code.DotNet.EndpointNotFound' }
        'CommunicationException'              { return Get-Text 'Value.Code.DotNet.Communication' }
        'CommunicationObjectFaultedException' { return Get-Text 'Value.Code.DotNet.CommunicationFaulted' }
        'FaultException'                      { return Get-Text 'Value.Code.DotNet.Fault' }
        'ServerTooBusyException'              { return Get-Text 'Value.Code.DotNet.ServerTooBusy' }
        'SecurityNegotiationException'        { return Get-Text 'Value.Code.DotNet.SecurityNegotiation' }
        'MessageSecurityException'            { return Get-Text 'Value.Code.DotNet.MessageSecurity' }
        'AuthenticationException'             { return Get-Text 'Value.Code.DotNet.Authentication' }
        'WebException'                        { return Get-Text 'Value.Code.DotNet.Web' }
        'HttpRequestException'                { return Get-Text 'Value.Code.DotNet.HttpRequest' }
        'SocketException'                     { return Get-Text 'Value.Code.DotNet.Socket' }
        'TimeoutException'                    { return Get-Text 'Value.Code.DotNet.Timeout' }
        'TaskCanceledException'               { return Get-Text 'Value.Code.DotNet.TaskCanceled' }
        'OperationCanceledException'          { return Get-Text 'Value.Code.DotNet.TaskCanceled' }
        'IOException'                         { return Get-Text 'Value.Code.DotNet.IO' }
        'FileNotFoundException'               { return Get-Text 'Value.Code.DotNet.FileNotFound' }
        'DirectoryNotFoundException'          { return Get-Text 'Value.Code.DotNet.DirectoryNotFound' }
        'UnauthorizedAccessException'         { return Get-Text 'Value.Code.DotNet.UnauthorizedAccess' }
        'SqlException'                        { return Get-Text 'Value.Code.DotNet.Sql' }
        'OutOfMemoryException'                { return Get-Text 'Value.Code.DotNet.OutOfMemory' }
        'NullReferenceException'              { return Get-Text 'Value.Code.DotNet.NullReference' }
        'InvalidOperationException'           { return Get-Text 'Value.Code.DotNet.InvalidOperation' }
        'ObjectDisposedException'             { return Get-Text 'Value.Code.DotNet.ObjectDisposed' }
        'ArgumentException'                   { return Get-Text 'Value.Code.DotNet.Argument' }
        'ArgumentNullException'               { return Get-Text 'Value.Code.DotNet.Argument' }
        'COMException'                        { return Get-Text 'Value.Code.DotNet.Com' }
    }
    Get-Text 'Value.Code.DotNet.Unknown'
}

function Format-DotNetException {
    <#
    .SYNOPSIS
        A .NET exception type with what it means behind it, "EndpointNotFoundException
        (unter der Adresse antwortet kein Dienst)"; what is no exception's name as it is.
        Pure.
    #>

    [CmdletBinding()]
    [OutputType([string])]
    param([AllowNull()][AllowEmptyString()][string]$Name)

    Format-CodeWithMeaning -Code "$Name".Trim() -Meaning (Get-DotNetExceptionText -Name $Name)
}