Get-DbgObject.ps1

param(
    [Parameter(ValueFromPipelineByPropertyName = $true)]
    $Address,

    [Switch]
    $ExcludeFields,

    [String]
    $BreadCrumb
)

begin
{   
    function unstrict { Set-StrictMode -Off; . $args[0] }
    Set-StrictMode -Version Latest
}

process
{
    if(-not $BreadCrumb) { $BreadCrumb = $Address }

    ## Ensure the address is of the proper width. If not, it's probably
    ## a field value.
    if($Address.Length -ne 16)
    {
        return
    }

    ## Check if the address is 0, some variation of that, or negative.
    ## In this case, it's probably also a field value.
    $intAddress = $Address -as [Decimal]
    if(($intAddress -is [Decimal]) -and
        ((-not $intAddress) -or ($intAddress -le 0)))
    {
        return
    }

    ## Prepare the !DumpObject command
    $command = "!DumpObj"
    if($ExcludeFields)
    {
        $command += " -NoFields"
    }
    $command += " $Address"

    ## Invoke it, and start preparing the results
    $results = @(Invoke-DbgCommand $command)
    $returnObject = New-Object PSObject -Property  @{
        Type = ""
        Size = 0
        Address = $Address
        Path = $BreadCrumb
        Fields = @{}
    }

    ## Check if there were any errors.
    if($results.Length -eq 1)
    {
        Write-Error ("Could not analyze object ($Address) : " + $results)
        return
    }
    
    ## Pick out the fields
    foreach($result in $results)
    {
        ## If it was a name:value pair, split it on that
        if($result -match ": ")
        {
            $fields = $result -split ': ' | Foreach-Object Trim
        }
        ## Otherwise, do general parsing on columns
        else
        {
            $fields = $result -split '\s+' | Foreach-Object Trim
        }

        ## Handle properties we want to remap
        if($fields[0] -eq "Name")
        {
            $type = $fields[1]
            $returnObject.Type = $type
            continue
        }
        if($fields[0] -eq "Size")
        {
            $size = $fields[1]
            $size = ($size -split '\(')[0]
            $returnObject.Size = $size
            continue
        }

        ## Handle other generic properties
        if($fields.Length -eq 2)
        {
            $field = ($fields[0] -split ': ')[0].Trim()
            if($field)
            {
                Add-Member -in $returnObject NoteProperty $field $fields[1].Trim()
            }
            continue
        }

        ## Get information about the fields
        if(-not $ExcludeFields)
        {
            ## This is a field
            if($fields.Length -eq 8)
            {
                $methodTable = $fields[0]
                $field = $fields[1]
                $offset = $fields[2]
                $type = $fields[3]
                $vt = $fields[4]
                $attr = $fields[5]
                $address = $fields[6]
                $name = $fields[7]

                ## Fix the name if needed / possible. The default view often
                ## truncates this.
                if($type -match "\.\.\.")
                {
                    $mtResults = Invoke-DbgCommand "!dumpmt $methodTable"
                    $mtName = $mtResults[2]
                    $mtName = ($mtName -split ': ')[1]
                    $type = $mtName
                }

                ## Now, if it's an object, follow the reference to see what it
                ## really is
                if($type -eq "System.Object")
                {
                    $actualField = Get-DbgObject -Address $address -ExcludeFields
                    $type = $actualField.Type
                }

                ## Start preparing the field to return
                $returnField = New-Object PSObject -Property @{
                    MethodTable = $methodTable;
                    Field = $field;
                    Offset = $offset;
                    Type = $type;
                    VirtualTable = $vt;
                    Attribute = $attr;
                    Address = $address;
                    Name = $name
                }

                ## Store the rich data in the Fields property of ReturnObject
                $field = (New-Object PSObject $returnField |
                     Select-Object Name,Address,Type,MethodTable,Field,Offset,VirtualTable,Attribute)
                $returnObject.Fields[$name] = $field

                ## Now, process the field to add user-friendly data
                $visualResult = "{0} {1} @ {2}" -f $field.Attribute,$field.Type,$field.Address

                ## Try to do some basic type conversions
                try { $realType = [Type] $type }
                catch { $realType = [System.Object] }

                ## If it was a string, follow the reference to get its value
                if($realType -eq [System.String])
                {
                    $actualField = Get-DbgObject -Address $address
                    $visualResult = unstrict { $actualField.String }
                }
                ## If it's a value type, try to get its value as well
                elseif($realType.IsValueType)
                {
                    ## The value for value types is often buried in the reference, under
                    ## a "value" field. @TODO
                    $result = Get-DbgObject $address
                    if($result)
                    {
                        $valueFields = @($result.Fields.GetEnumerator() | ? { $_.Name -match "value" })
                        $value = $valueFields[0].Value.Address
                    }
                    ## Otherwise, the value is in the Address field
                    else
                    {
                        $value = $address
                    }

                    ## Many things convert via Int, so try to convert the string to an int
                    if($value -as [Int]) { $value = $value -as [Int] }

                    ## Now try to convert it to the original type
                    if($value -as $realType) { $value = $value -as $realType }

                    ## And remember the visual result
                    $visualResult = $value
                }

                ## Store the visual resule
                Add-Member -in $returnObject NoteProperty $name $visualResult

                ## Create a navigation breadcrumb so that we can follow references
                ## around.
                $newBreadCrumb = $breadCrumb + "." + $name
                Add-Member -in $returnObject ScriptMethod Get$name {
                    Get-DbgObject -Address $address -BreadCrumb $newBreadCrumb
                }.GetNewClosure()
            }
        }
    }

    ## Output the final object
    $returnObject        
}