Private/Reading.ps1

# A reading of the Main Part, filled in from the one the Elevated Part took.
#
# A Check in the Session of the User may not read everything: the processes, the Sessions
# and the profile folders of another account answer only with admin rights. Since 0.10 a
# Kind that needs them is read a second time in the Elevated Part, by a Kind of its own
# that judges nothing, and the Check of the Main Part is handed that reading and fills in
# what it could not read itself (see the header of Kinds/Program.ps1 and of
# Kinds/Session.ps1).
#
# The filling in is the same wherever it is done, and is done here, once:
#
# - the rows of the Main Part are the ones that count: one comes out for each, in their
# order, and a row only the Elevated Part has is not added. It was read earlier, and
# what it describes may have ended since;
# - a row is matched to the row of the Elevated Part with the same key, and only where
# that is the same thing still: Windows gives the number of a process or of a Session
# to another one once its first owner has ended;
# - what the Main Part read itself stays. It was read later. Only what it left absent is
# taken from the other row;
# - which rows the filling is for, the Kind says.
#
# The Program and the Session Kind fill in by it. The UserProfile Kind does not: its rule
# is another one, field by field - a User Profile only the Elevated Part saw is added, as
# it is on the disk whoever saw it; the kind an administrator read replaces one this
# account read as local; and of two dates of last use the one from the registry comes
# first, whichever reading has it. See ConvertTo-UserProfileOtherRow.
#
# Why something is still unread afterwards is one of two things, and a Report tells them
# apart: nobody read with admin rights in this Run, or Windows did not give it with admin
# rights either. Which it is, is a fact about the Run and not about a row: see
# Get-UnreadReason.
#
# Pure, all of it: rows in, rows out.

function Test-ReadingAbsent {
    <#
    .SYNOPSIS
        Whether a value of a reading is not there: nothing, or text with nothing in it.
        Zero and "no" are there. Pure.
    #>

    [CmdletBinding()]
    [OutputType([bool])]
    param([AllowNull()]$Value)

    if ($null -eq $Value) { return $true }
    if ($Value -is [string]) { return -not $Value.Trim() }
    $false
}

function Join-ElevatedReading {
    <#
    .SYNOPSIS
        The rows the Main Part read, each with what it left absent filled in from the row
        of the Elevated Part that has the same key and is the same thing. Pure.
    .DESCRIPTION
        One result for each row of the Main Part, in the order they came in:
 
            Row the row, with the properties named in Take filled in where they
                      were absent and the other row has them. The very row that came in
                      where nothing was taken; a copy of it with every property it had
                      where something was
            Elevated the row of the Elevated Part with the same key, whether or not it
                      was taken from, or nothing. For what a Kind has to ask of it beyond
                      the filling
            Filled whether anything was taken
 
        Two rows of the Elevated Part with one key: the later one counts.
    .PARAMETER Row
        The rows as the Main Part read them.
    .PARAMETER Elevated
        The rows as the Elevated Part read them, or nothing where it did not.
    .PARAMETER Key
        The property both are matched by. Compared as text.
    .PARAMETER Take
        The properties that are filled in from the row of the Elevated Part where the row
        of the Main Part has them absent.
    .PARAMETER Unread
        Says of a row of the Main Part whether the filling is for it: handed the row,
        answers yes or no. Without it, the filling is for every row that has one of the
        properties to take absent.
    .PARAMETER Same
        Says whether the row of the Elevated Part is still the same thing as the row of
        the Main Part, and one to take from: handed the two, in that order, answers yes
        or no. Without it, the same key is enough.
    #>

    [CmdletBinding()]
    [OutputType([psobject])]
    param(
        [AllowNull()][AllowEmptyCollection()]$Row,
        [AllowNull()][AllowEmptyCollection()]$Elevated,
        [Parameter(Mandatory)][string]$Key,
        [Parameter(Mandatory)][string[]]$Take,
        [scriptblock]$Unread,
        [scriptblock]$Same
    )

    $byKey = @{}
    foreach ($other in @($Elevated | Where-Object { $null -ne $_ })) { $byKey["$(Get-DataProperty $other $Key)"] = $other }

    foreach ($own in @($Row | Where-Object { $null -ne $_ })) {
        $other = $byKey["$(Get-DataProperty $own $Key)"]

        $absent = @($Take | Where-Object { Test-ReadingAbsent (Get-DataProperty $own $_) })
        $wanted = $(if ($Unread) { [bool](& $Unread $own) } else { [bool]$absent.Count })
        $usable = [bool]($wanted -and $null -ne $other -and (-not $Same -or [bool](& $Same $own $other)))
        $taken  = @($absent | Where-Object { $usable -and -not (Test-ReadingAbsent (Get-DataProperty $other $_)) })

        $merged = $own
        if ($taken.Count) {
            $copy = [ordered]@{}
            if ($own -is [System.Collections.IDictionary]) { foreach ($name in $own.Keys) { $copy["$name"] = $own[$name] } }
            else { foreach ($property in $own.PSObject.Properties) { $copy[$property.Name] = $property.Value } }
            foreach ($name in $taken) { $copy[$name] = Get-DataProperty $other $name }
            $merged = [pscustomobject]$copy
        }

        [pscustomobject]@{ Row = $merged; Elevated = $other; Filled = [bool]$taken.Count }
    }
}

function Get-UnreadReason {
    <#
    .SYNOPSIS
        Why something a Check could not read itself is still unread: 'NotAsked' where
        nobody read with admin rights in this Run, 'Refused' where the Elevated Part read
        and Windows did not give it even then. Pure.
    .DESCRIPTION
        Asked of the data of the Check, which holds as ElevatedRead whether the reading
        with admin rights was handed to it. That it was, and what is absent all the same,
        is what "not readable with admin rights either" means.
    .PARAMETER Data
        The data of the Check, or the part of it that holds ElevatedRead.
    #>

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

    if ([bool](Get-DataProperty $Data 'ElevatedRead')) { return 'Refused' }
    'NotAsked'
}

function Get-UnreadText {
    <#
    .SYNOPSIS
        What a cell says in place of a value that is not there: "nicht gelesen" or "nicht
        lesbar". Pure.
    .DESCRIPTION
        Two wordings, and each means one thing wherever a Report says it:
 
            nicht gelesen Gutcheck did not read it in this Run: nobody asked with admin
                            rights, the Check that reads it did not run, or the Run is
                            not in the Session of the User
            nicht lesbar it was asked for, and Windows did not give it
 
        A Technician who reads the first runs Gutcheck again, with admin rights or in the
        right Session. One who reads the second need not: it will say the same.
 
        Where a Kind fills in from the Elevated Part, which of the two it is follows from
        Get-UnreadReason, and the Kind hands that on. A Kind that asked Windows itself and
        was given nothing says 'Refused'.
    .PARAMETER Reason
        'NotAsked' or 'Refused': see Get-UnreadReason.
    #>

    [CmdletBinding()]
    [OutputType([string])]
    param([Parameter(Mandatory)][ValidateSet('NotAsked', 'Refused')][string]$Reason)

    if ($Reason -eq 'Refused') { return Get-Text 'Value.Cell.Unreadable' }
    Get-Text 'Value.Cell.NotRead'
}