Private/QrCode.ps1

# The QR code a Technician points a phone camera at.
#
# The device flow asks the Technician to open one page on their own phone, and that page is
# the same every time. So the symbol for it is computed once, at development time, and
# shipped here as data - rather than encoding a URL at runtime, which would mean carrying a
# Reed-Solomon implementation inside a diagnostics tool. That is exactly the sort of code
# that is subtly wrong for a year and then discovered by a Technician standing at a
# Customer Site with a phone that will not scan.
#
# Generated with QRCoder 1.3.3 at error correction level M, which puts this at version 3:
# 29x29 modules. It encodes $script:DeviceLoginUri and nothing else. Regenerating it means
# replacing the whole table; the tests assert the structure every QR symbol must have -
# finder patterns, both timing patterns, the alignment pattern - so a truncated or mangled
# paste fails the suite instead of shipping as a square that scans to nothing.
#
# To regenerate, if the address ever changes. The module is a development tool and is not
# a dependency of Gutcheck:
#
# Install-Module QRCodeGenerator -Scope CurrentUser
# Import-Module QRCodeGenerator # loads QRCoder alongside it
# $d = [QRCoder.QRCodeGenerator]::new().CreateQrCode(
# 'https://login.microsoft.com/device', [QRCoder.QRCodeGenerator+ECCLevel]::M)
# $m = $d.ModuleMatrix # includes a 4-module quiet zone, which we re-add ourselves
# for ($y = 4; $y -lt $m.Count - 4; $y++) {
# -join (4..($m[$y].Count - 5) | ForEach-Object { if ($m[$y][$_]) { '#' } else { '.' } })
# }

$script:DeviceLoginUri = 'https://login.microsoft.com/device'

$script:DeviceLoginQr = @(
    '#######...######...##.#######'
    '#.....#..#..#####.....#.....#'
    '#.###.#.##..##.#..###.#.###.#'
    '#.###.#.#.....##.##...#.###.#'
    '#.###.#.##.#...######.#.###.#'
    '#.....#.#.###....#..#.#.....#'
    '#######.#.#.#.#.#.#.#.#######'
    '........###...#.#.###........'
    '#.#####..###.....#.#..#####..'
    '...#.#.##.######..#######...#'
    '#....###...#.######.#.###....'
    '##..#..#.#...#.##..#.##..#.#.'
    '####..#.......##.#..##...##..'
    '.##.#...#.###...#########...#'
    '#.###.#.##..#..###..#.##.##..'
    '##.###.#..###.#....######..#.'
    '.#.#####.#.##..#.#.#.....##..'
    '#.#.......#..##.#########.#.#'
    '#.#.#.#....#.####.....##..#..'
    '#...##...##..#.#...#...#...#.'
    '#.#.###.#.....#..##.#####.###'
    '........#.##....#####...#####'
    '#######....##..#.#.##.#.###..'
    '#.....#.#.###.#.#..##...#..##'
    '#.###.#.#..#...###..#####.##.'
    '#.###.#.#..##.#.#.###....####'
    '#.###.#.#...#..##....#######.'
    '#.....#..##.#..##.#.###.##.#.'
    '#######.####.###.#.#...#..#..'
)

# A symbol printed hard against a dark console is a symbol no camera resolves. Four modules
# is what the specification asks for and what scanners are built to expect.
$script:QrQuietZone = 4

# Every line a Run prints under the symbol starts three spaces in, and a square that starts
# at column zero reads as though it belongs to something else. Written with no colour, so
# it is terminal background rather than more quiet zone - otherwise the white block starts
# where it started before and nothing looks aligned.
$script:QrIndent = ' '

# U+2580 UPPER HALF BLOCK. A console cell is about twice as tall as it is wide, so a module
# drawn as one cell comes out stretched, and a module drawn as two spaces comes out square
# at four times the area. This character splits the cell instead: its foreground colour is
# the module above, its background colour the module below. Two rows to a line, one column
# to a module, and the modules land square at a quarter of the size.
$script:QrHalfBlock = [char]0x2580

function Test-QrHalfBlockRenders {
    <#
    .SYNOPSIS
        Whether this console can print the half block at all.
    .DESCRIPTION
        Asked rather than assumed. It survives the OEM code pages Windows consoles default
        to - CP850 and CP437 both carry it - but a console set to something that cannot
        represent it would draw a grid of substitution marks, which is a worse QR code than
        no QR code. The round trip is the honest test: encode the character the way this
        console will, decode it back, and see whether it is still itself.
    #>

    [CmdletBinding()]
    [OutputType([bool])]
    param()

    try {
        $encoding = [Console]::OutputEncoding
        if (-not $encoding) { return $false }
        [bool]($encoding.GetString($encoding.GetBytes($script:QrHalfBlock)) -eq [string]$script:QrHalfBlock)
    }
    catch { $false }
}

function Get-QrDisplayWidth {
    <#
    .SYNOPSIS
        How many columns drawing the symbol would take. Pure.
    #>

    [CmdletBinding()]
    [OutputType([int])]
    param(
        [Parameter(Mandatory)][int]$Modules,
        [switch]$Wide
    )

    $across = $Modules + (2 * $script:QrQuietZone)
    if ($Wide) { return ($across * 2) + $script:QrIndent.Length }
    $across + $script:QrIndent.Length
}

function Test-QrFits {
    <#
    .SYNOPSIS
        Whether this console is wide enough to draw the symbol undistorted.
    .DESCRIPTION
        A symbol that wraps is not a symbol, it is two halves of one - and it would scroll
        the code and the URL out of sight on the way past. A narrow window is a reason to
        print the address and stop, not a reason to draw something unreadable.
    #>

    [CmdletBinding()]
    [OutputType([bool])]
    param(
        [Parameter(Mandatory)][int]$Modules,
        [switch]$Wide
    )

    $needed = Get-QrDisplayWidth -Modules $Modules -Wide:$Wide
    try { $available = $Host.UI.RawUI.WindowSize.Width }
    catch { return $false }

    # A host that reports nothing sensible - a redirected stream, a remote session - is one
    # where drawing a picture is not the helpful choice.
    if (-not $available -or $available -le 0) { return $false }
    $available -ge $needed
}

function Get-QrPaddedRow {
    <#
    .SYNOPSIS
        The symbol's rows with the quiet zone around them. Pure.
    .DESCRIPTION
        The quiet zone is drawn rather than left to the console, because a console's
        background is whatever the Technician chose and a camera needs light. An odd number
        of rows gains one more light row, so that the half blocks pair up evenly.
    #>

    [CmdletBinding()]
    [OutputType([string[]])]
    param([Parameter(Mandatory)][string[]]$Row, [switch]$EvenRows)

    $blank = '.' * ($Row[0].Length + (2 * $script:QrQuietZone))
    $side  = '.' * $script:QrQuietZone

    $padded = New-Object System.Collections.Generic.List[string]
    for ($i = 0; $i -lt $script:QrQuietZone; $i++) { $padded.Add($blank) }
    foreach ($line in $Row) { $padded.Add($side + $line + $side) }
    for ($i = 0; $i -lt $script:QrQuietZone; $i++) { $padded.Add($blank) }
    if ($EvenRows -and ($padded.Count % 2)) { $padded.Add($blank) }

    $padded.ToArray()
}

function Get-QrColour {
    <#
    .SYNOPSIS
        A module's colour. Set explicitly, both of them: whatever a Technician has made
        their console look like, a QR code is black on white.
    #>

    [CmdletBinding()]
    [OutputType([string])]
    param([Parameter(Mandatory)][char]$Module)

    if ($Module -eq '#') { 'Black' } else { 'White' }
}

function Write-QrLinePair {
    <#
    .SYNOPSIS
        Two rows of modules on one console line, written in runs rather than per module -
        which is the difference between drawing instantly and drawing visibly.
    #>

    [CmdletBinding()]
    param(
        [Parameter(Mandatory)][AllowEmptyString()][string]$Upper,
        [Parameter(Mandatory)][AllowEmptyString()][string]$Lower
    )

    Write-Host $script:QrIndent -NoNewline

    $start = 0
    for ($i = 1; $i -le $Upper.Length; $i++) {
        $changed = $i -eq $Upper.Length -or $Upper[$i] -ne $Upper[$start] -or $Lower[$i] -ne $Lower[$start]
        if ($changed) {
            Write-Host ([string]$script:QrHalfBlock * ($i - $start)) -NoNewline `
                -ForegroundColor (Get-QrColour -Module $Upper[$start]) `
                -BackgroundColor (Get-QrColour -Module $Lower[$start])
            $start = $i
        }
    }
    Write-Host ''
}

function Write-QrWideRow {
    <#
    .SYNOPSIS
        One row of modules as two-space blocks, for a console that cannot print the half
        block. Twice as wide and twice as tall, and made of spaces, which every console can
        draw whatever its code page.
    #>

    [CmdletBinding()]
    param([Parameter(Mandatory)][AllowEmptyString()][string]$Row)

    Write-Host $script:QrIndent -NoNewline

    $start = 0
    for ($i = 1; $i -le $Row.Length; $i++) {
        if ($i -eq $Row.Length -or $Row[$i] -ne $Row[$start]) {
            Write-Host (' ' * ($i - $start)) -NoNewline -BackgroundColor (Get-QrColour -Module $Row[$start])
            $start = $i
        }
    }
    Write-Host ''
}

function Get-QrCardLine {
    <#
    .SYNOPSIS
        Two rows of modules as one line of a card: the half blocks with their colours in
        the line itself, as a terminal that draws reads them. Pure.
    .DESCRIPTION
        A row of a card is text, and has no colour behind it to set. So the colours are
        written into the text, both of them for every run of modules, which is what keeps
        the symbol black on white whatever the window looks like. Lantern counts the
        columns of such a line without them and puts its border where it belongs.
    #>

    [CmdletBinding()]
    [OutputType([string])]
    param(
        [Parameter(Mandatory)][AllowEmptyString()][string]$Upper,
        [Parameter(Mandatory)][AllowEmptyString()][string]$Lower
    )

    $escape = [string][char]27
    $line   = New-Object System.Text.StringBuilder
    $start  = 0
    for ($i = 1; $i -le $Upper.Length; $i++) {
        if ($i -eq $Upper.Length -or $Upper[$i] -ne $Upper[$start] -or $Lower[$i] -ne $Lower[$start]) {
            # 30 and 97 are black and white as a foreground, 40 and 107 as a background.
            $fore = $(if ($Upper[$start] -eq '#') { 30 } else { 97 })
            $back = $(if ($Lower[$start] -eq '#') { 40 } else { 107 })
            [void]$line.Append(('{0}[{1};{2}m{3}{0}[0m' -f $escape, $fore, $back, ([string]$script:QrHalfBlock * ($i - $start))))
            $start = $i
        }
    }
    $line.ToString()
}

function Show-DeviceLoginQr {
    <#
    .SYNOPSIS
        Draws the QR code for the device login page, and says whether it drew it.
    .DESCRIPTION
        The shipped symbol encodes one fixed address. If Entra ever answers with a
        different verification_uri, drawing it would point a Technician's phone at the
        wrong page - a failure that looks like the phone's fault and is not. So the address
        is compared first, and anything unexpected falls back to printing what Entra
        actually said.
    #>

    [CmdletBinding()]
    [OutputType([bool])]
    param([Parameter(Mandatory)][AllowNull()][AllowEmptyString()][string]$VerificationUri)

    if ($VerificationUri -ne $script:DeviceLoginUri) { return $false }

    if ($script:ConsolePart) {
        # In a card, where the window is drawn in: the half block always renders there,
        # and what has to fit is the card. A symbol that does not is not drawn: cut or
        # wrapped it is no symbol.
        $rows = Get-QrPaddedRow -Row $script:DeviceLoginQr -EvenRows
        if ($rows[0].Length -gt (Get-ConsoleCardRoom)) { return $false }
        for ($i = 0; $i -lt $rows.Count; $i += 2) { Write-LanternLine (Get-QrCardLine -Upper $rows[$i] -Lower $rows[$i + 1]) }
        return $true
    }

    $compact = Test-QrHalfBlockRenders
    if (-not (Test-QrFits -Modules $script:DeviceLoginQr.Count -Wide:(-not $compact))) { return $false }

    Write-Host ''
    if ($compact) {
        $rows = Get-QrPaddedRow -Row $script:DeviceLoginQr -EvenRows
        for ($i = 0; $i -lt $rows.Count; $i += 2) { Write-QrLinePair -Upper $rows[$i] -Lower $rows[$i + 1] }
    }
    else {
        foreach ($row in (Get-QrPaddedRow -Row $script:DeviceLoginQr)) { Write-QrWideRow -Row $row }
    }
    Write-Host ''
    $true
}