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 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 }

    $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
}