Stop using Google to lookup error codes and write your own error lookup class.
Tags: csharp, utilities
Published: 2024-01-07 15:53:38

Creating a class to decode EVERY Windows error code in C#.

I got tired of dealing with NTStatus, HResults, Win32 error codes, etc., so I wrote my own error lookup program.

First, we need the error codes. There are multiple resources that have all the error codes, but the easiest way to obtain them is to download the Microsoft Error Lookup Tool and export the embedded error codes to a .csv file. You can get it from https://www.microsoft.com/en-US/download/details.aspx?id=100432 or search for it if the link is broken.

You can export all of the error codes with the command: Err_6.4.5.exe /:outputtoCSV > errors.txt.

Next, I compress the file. I am choosing Brotli compression. There is a method in the Utils class to compress the file.

After compressing the file, I embed the file as a resource in Visual Studio Resources property page.

Example output:
```
Only show ntstatus and winerror records? Y/N. n

Enter error to lookup, e.g. 1234, 0x0a345.
123

Source: bugcodes.h  Error Code: Dec: 123  Hex: 7B
Symbolic Name(s): INACCESSIBLE_BOOT_DEVICE
Description(s): N/A

Source: netmon.h  Error Code: Dec: 123  Hex: 7B
Symbolic Name(s): NMERR_SECURITY_BREACH_CAPTURE_DELETED
Description(s): N/A

Source: winerror.h  Error Code: Dec: 123  Hex: 7B
Symbolic Name(s): ERROR_INVALID_NAME
Description(s): The filename, directory name, or volume label syntax is incorrect.

Enter error to lookup, e.g. 1234, 0x0a345.
0x80070643

As win32 error:

Source: winerror.h  Error Code: Dec: 1603  Hex: 643
Symbolic Name(s): ERROR_INSTALL_FAILURE
Description(s): Fatal error during installation.
```

The following code is heavily commented and written using .NET 8, but would only require very minor changes to downgrade.
```
using System.Globalization;
using System.IO.Compression;
using System.Text;
using static Errors.Utils;

namespace Errors;

internal class ErrorRecord
{
    private int ErrorCode;
    private string? SymbolicName, Description, Source;

    private static readonly CultureInfo culture = new("en-US");
    private static readonly Dictionary<int, List<ErrorRecord>> errorlist = CreateErrorDatabase();

    internal static Dictionary<int, List<ErrorRecord>> CreateErrorDatabase()
    {
        Dictionary<int, List<ErrorRecord>> dict = [];

        //Retrieve and decompress embedded resource.
        using MemoryStream errorstream = new(Properties.Resources.errors);
        using BrotliStream decompressStream = new(errorstream, CompressionMode.Decompress);
        using StreamReader reader = new(decompressStream);

        List<ErrorRecord> initialrecordset = [];

        IEnumerable<string> allrecords = reader.ReadToEnd()
            .Replace("N/A", "", StringComparison.Ordinal)         //Remove "N/A" in descriptions. When we run .Distinct it will eliminate duplicate records that previously had one with N/A and the other had no description.
            .Replace("\"", "", StringComparison.Ordinal)          //Remove all quotes from text.
            .Replace("&apos;", "\'", StringComparison.Ordinal)    //Replace unicode apostrophe with corresponding character.
            .Replace("&quot;", "\"", StringComparison.Ordinal)    //Replace unicode quote with corresponding character.
            .Replace("&#10;", " ", StringComparison.Ordinal)      //Replace unicode line feed with a space.
            .Replace("&lt;", "<", StringComparison.Ordinal)       //Replace unicode 'less than' character with corresponding character.
            .Replace("&gt;", ">", StringComparison.Ordinal)       //Replace unicode 'greater than' with corresponding character.
            .Replace("&amp;", "&", StringComparison.Ordinal)      //Replace unicode 'and' with corresponding character.
            .Split(Environment.NewLine)                           //Split into those sweet error records, baby!
            .Skip(1)                                              //Remove duplicates.
            .Distinct()                                           //Skip header row.
            .Where(l => !string.IsNullOrEmpty(l));                //Remove blank line at the end file.

        foreach (string? line in allrecords)
        {
            string[] currentRow = line.Split(",");                //Split records into fields.

            initialrecordset.Add(new ErrorRecord()                //Parse fields and add to new records.
            {
                ErrorCode = int.Parse(currentRow![0].Replace("0x", "", StringComparison.Ordinal), NumberStyles.HexNumber, culture),

                SymbolicName = currentRow[1],

                Description = currentRow[2]
                .Replace("&#44;", ",", StringComparison.Ordinal)  //Replace unicode comma with comma. We must do this AFTER splitting the records into fields as they are comma delimmited.
                .CompactWhitespace(),                             //Clean up descriptions.

                Source = currentRow[3]
            });
        }

        //Exclude all records that have no description but have a duplicate with the same Source, SymbolicName, and ErrorCode that has a description.
        //These only show up in pairs in the csv and there are no pairs where both have an empty description.
        List<ErrorRecord> ErrorRecords = initialrecordset.Except(initialrecordset
            .GroupBy(x => new { x.Source, x.SymbolicName, x.ErrorCode })
            .Where(c => c.Count() > 1).SelectMany(a => a)
            .Where(d => string.IsNullOrEmpty(d.Description)))
            .ToList();

        var DuplicateRecords = ErrorRecords                       //Create set of records that contains records with the same Source and ErrorCode but different SymbolicNames.
            .GroupBy(x => new { x.Source, x.ErrorCode })
            .Where(c => c.Count() > 1)
            .ToList();

        List<ErrorRecord> CombinedDuplicates = [];

        foreach (var duplicates in DuplicateRecords)
        {
            string sb = string.Empty, d = string.Empty;
            int count = 0;                                        //Number SymbolicNames and Descriptions to make records easier to read.

        foreach (var duplicates in DuplicateRecords)
        {
            string sb = string.Empty, d = string.Empty;
            int count = 0;                                        //Number SymbolicNames and Descriptions to make records easier to read.
            foreach (ErrorRecord? record in duplicates)
            {
                sb += $"[{++count}] {record.SymbolicName}  "; //Combine all SymbolicNames from each duplicate in DuplicateRecords and number consecutively.

                d += !string.IsNullOrEmpty(record.Description) ? $"[{count}] {record.Description}  " : $"[{count}] N/A  ";  //Combine all Descriptions from each duplicate in DuplicateRecords and number consecutively.
            }

            //Add combined records to new record list.
            CombinedDuplicates.Add(new() { ErrorCode = duplicates.First().ErrorCode, Source = duplicates.First().Source, Description = d, SymbolicName = sb });
        }

            //Add combined records to new record list.
            CombinedDuplicates.Add(new() { ErrorCode = duplicates.First().ErrorCode, Source = duplicates.First().Source, Description = d, SymbolicName = sb });
        }

        //Remove DuplicateRecords from ErrorRecords and concatenate result with CombinedDuplicates,
        ErrorRecords = ErrorRecords.Except(DuplicateRecords.SelectMany(a => a)).Concat(CombinedDuplicates).ToList();

        //Add to dictionary for fast retrieval.
        foreach (ErrorRecord item in ErrorRecords)
            if (dict.TryGetValue(item.ErrorCode, out List<ErrorRecord>? er))
                er.Add(item);
            else
                dict.Add(item.ErrorCode, [item]);

        return dict;
    }


    //Retrieve all records for a specific error.
    public static List<ErrorRecord> GetErrorRecords(int error) => errorlist.TryGetValue(error, out List<ErrorRecord>? value) ? value : [];


    //Retrieve all records from a specific error and header, e.g., ntstatus.h.
    public static ErrorRecord? GetErrorRecordFromHeader(int error, string header) => errorlist.TryGetValue(error, out List<ErrorRecord>? value) ?
        value.Find(x => string.Equals(x.Source, header, StringComparison.OrdinalIgnoreCase)) : null;


    //Override .ToString method for console output.
    public override string ToString() => $"Source: {Source}  Error Code: Dec: {ErrorCode}  Hex: {ErrorCode:X}\nSymbolic Name(s): " +
        $"{SymbolicName}\n{(!string.IsNullOrEmpty(Description) ? $"Description(s): {Description}\n" : $"Description(s): N/A\n")}";


    //Retrieve error string from a specific header, e.g., ntstatus.h, for public use.
    public static string GetErrorFromHeader(int error, string header)
    {
        string? recordtostring;
        return errorlist.TryGetValue(error, out List<ErrorRecord>? value) &&
            (recordtostring = value.Find(x => string.Equals(x.Source, header, StringComparison.OrdinalIgnoreCase))?.ToString()) != null
            ? recordtostring : $"Dec: {error} Hex: {error:X} does not exist in the error database for the specified header.";
    }


    //Example method to retrieve errors from the command line. Also converts error to an HResult counterpart if applicable, similar (but apparently not identical to the Microsoft Error Lookup program).
    public static void GetErrors(bool ans)
    {
        int lookup = GetInt(caption: "Enter error to lookup, e.g. 1234, 0x0a345."), win32error = lookup & 0xFFFF;

        List<ErrorRecord> e1 = ans ? GetErrorRecords(lookup).Where(x => x.Source is "ntstatus.h" or "winerror.h").ToList() : GetErrorRecords(lookup),
            e2 = (lookup & 0xFFFF0000) == 0x80070000 ? GetErrorRecords(win32error).Where(x => x.Source is "winerror.h").ToList() : [];

        e1.PrintList();

        if (e2.Count != 0)
        {
            Console.WriteLine($"As win32 error:\n");
            e2.PrintList();
        }

        if (e1.Count == 0 && e2.Count == 0)
            Console.WriteLine("There are no matching errors in the error database.\n");
    }
}


internal static class Utils
{
    private static readonly CultureInfo culture = new ("en-US");

    //Compress .csv file before embedding.
    internal static void CompressFile(string name)
    {
        byte[] bytes = File.ReadAllBytes(name);

        using MemoryStream stream = new();

        using (BrotliStream brotli = new(stream, CompressionLevel.Optimal))
            brotli.Write(bytes, 0, bytes.Length);

        File.WriteAllBytes(Environment.GetFolderPath(Environment.SpecialFolder.Desktop) + "\\" + Path.GetFileNameWithoutExtension(name) + ".brotli", stream.ToArray());
    }

    //Read from command line until a valid hexadecimal or number is read.
    internal static int GetInt(int min = int.MinValue, int max = int.MaxValue, string caption = "")
    {
        if (!string.IsNullOrWhiteSpace(caption))
            Console.WriteLine(caption);

        string? ans;
        int num;

        while (string.IsNullOrWhiteSpace(ans = Console.ReadLine()) || !(ans.Contains('x', StringComparison.Ordinal) || ans.Contains('X', StringComparison.Ordinal)
            ? int.TryParse(ans.Replace("0x", "", StringComparison.Ordinal).Replace("0X", "", StringComparison.Ordinal), NumberStyles.HexNumber, culture, out num)
            : int.TryParse(ans, out num)) || !(num >= min && num <= max)) ;

        Console.WriteLine();

        return num;
    }

    //Read from command line for Y/N response..
    internal static bool GetYN(string caption = "")
    {
        if (!string.IsNullOrWhiteSpace(caption))
            Console.Write(caption);

        char x = Console.ReadKey().KeyChar;

        Console.WriteLine('\n');

        return x is 'y' or 'Y';
    }

    //A simple print extension.
    internal static void PrintList<T>(this IEnumerable<T> col)
    {
        foreach (T item in col)
        {
            string? line = item?.ToString();

            if (!string.IsNullOrEmpty(line))
                Console.WriteLine(line);
        }
    }

    //An extension to compact the whitespace in a string.
    internal static string CompactWhitespace(this string str)
    {
        if (string.IsNullOrWhiteSpace(str))
            return string.Empty;

        str = str.Trim();

        StringBuilder sb = new();

        for (int i = 0; i < str.Length - 1; i++)
        {
            if (char.IsWhiteSpace(str[i]) && char.IsWhiteSpace(str[i + 1]))
                continue;

            _ = char.IsWhiteSpace(str[i]) ? sb.Append(' ') : sb.Append(str[i]);
        }

        return !char.IsWhiteSpace(str[^1]) ? sb.Append(str[^1]).ToString() : sb.ToString();
    }
}

internal class Program
{
    static void Main()
    {
        bool ans = GetYN(caption: "Only show ntstatus and winerror records? Y/N. ");

        while (true)
            ErrorRecord.GetErrors(ans);
    }
}
```