Skip to main content
1 of 8
Minh Tran
  • 285
  • 1
  • 10

Design feedback for code that reads/writes to bytes of an ascii-encoded hex string

Problem Statement

Given a string of ascii-encoded hex characters (hascii), design classes to parse all byte-fields given by a binary specification, cast bytes into suitable types for storage, and allow the user to write to a byte-field. For this purpose, I've created a simple binary specification:

string hasciiData = "07E30A0240490FD8402df8548000000048656C6C6F576F726C64218000000000000000";

With the following binary specification:

        string _year = "07E3"; // 2019  16-bit int, 0x07E3
        string _month = "0A"; // 10   8-bit int, 0x0A
        string _day = "02"; // 2   8-bit int, 0x02
        string _pi = "40490FD8"; // IEEE-754 Single Precision Float, 0x4049 0FD8
        string _eulers = "402DF854"; // IEEE-754 Single Precision Float, 0x402D F854
        string _secretValue = "80000000"; // 32-bit int, 0x80000000 (Decimal -2147483648)
        string _secretMsg = "48656C6C6F576F726C6421"; // ASCII string "HelloWorld!", 0x48656C6C6F576F726C6421 
        string _bigInt = "8000000000000000"; // 64-bit int, 0x8000 0000 0000 0000 (Decimal -9223372036854775808)

Class Design

The design separates byte-level specification from the code that reads bytes from the string. The "specification" encapsulates the logical structure of the hascii data (storing details like the names of byte fields, byte offsets and field lengths) so that it can be used to parse (read) and write to the hascii string.

Manipulation of hascii characters (hascii bytes are pairs of hascii characters) are handled by the HasciiParser class.

Code

using System;
using System.Collections.Generic;
using System.Text;

using ByteSpecification = System.Collections.Generic.Dictionary<string, (int, int)>;
using CastOperation = System.Collections.Generic.Dictionary<string, System.Func<string, object>>;

namespace ByteParsing
{
    public class HasciiParser
    {
        public static string HexToString(string hascii)
        {
            StringBuilder sb = new StringBuilder();
            for (int i = 0; i < hascii.Length; i += 2)
            {
                string hexByte = hascii.Substring(i, 2);
                sb.Append(Convert.ToChar(Convert.ToUInt32(hexByte, 16)));
            }
            return sb.ToString();
        }

        public static string StringToHex(string str, int maxWidth, bool padLeft=true, char padChar='0')
        {
            StringBuilder sb = new StringBuilder();
            foreach (char c in str)
            {
                sb.AppendFormat("{0:X}", Convert.ToInt32(c));
            }

            if (padLeft)
            {
                return sb.ToString().PadLeft(maxWidth, padChar);
            }
            return sb.ToString().PadRight(maxWidth, padChar);
        }

        public DateTime HexToDatetime(string hascii)
        {
            int year = Convert.ToInt32(hascii.Substring(0, 4), 16);
            int month = Convert.ToInt32(hascii.Substring(4, 2), 16);
            int day = Convert.ToInt32(hascii.Substring(6, 2), 16);
            return new DateTime(year, month, day);
        }

        public static float HexToFloat(string hascii)
        {
            // https://stackoverflow.com/a/7903300/3396951
            uint num = uint.Parse(hascii, System.Globalization.NumberStyles.AllowHexSpecifier);
            byte[] floatVals = BitConverter.GetBytes(num);
            return BitConverter.ToSingle(floatVals, 0);
        }
    }

    public class PD0Format
    {
        public string _hasciiData; // Normally private

        public PD0Format(string hasciiData)
        {
            _hasciiData = hasciiData;
        }

        protected CastOperation castop = new CastOperation()
        {
            {"Year", (hascii_str) => Convert.ToInt32(hascii_str, 16)},
            {"Month", (hascii_str) => Convert.ToInt32(hascii_str, 16)},
            {"Day", (hascii_str) => Convert.ToInt32(hascii_str, 16)},
            {"Pi", (hascii_str) => HasciiParser.HexToFloat(hascii_str)},
            {"EulersNumber", (hascii_str) => HasciiParser.HexToFloat(hascii_str)},
            {"SecretValue", (hascii_str) => Convert.ToInt32(hascii_str, 16)},
            {"SecretMessage", (hascii_str) => HasciiParser.HexToString(hascii_str)},
            {"BigInt", (hascii_str) => Convert.ToInt64(hascii_str, 16)}
        };

        protected ByteSpecification dateSpec = new ByteSpecification()
        {
            {"Year", (1, 2)},
            {"Month", (3, 1)},
            {"Day", (4, 1)},
        };

        protected ByteSpecification mathConstantsSpec = new ByteSpecification()
        {
            {"Pi", (5, 4)},
            {"EulersNumber", (9, 4)}
        };

        protected ByteSpecification secretsSpec = new ByteSpecification()
        {
            {"SecretValue", (13, 4)},
            {"SecretMessage", (17, 11)},
            {"BigInt", (28, 8)}
        };

        private string GetHasciiBytes(ByteSpecification byteSpec, string fieldName)
        {
            // ByteSpecification assumes base-1 indexing. Substring requires base-0 indexing so we must subtract 2 (reason below).
            //      Because there are two hex characters to a byte, we have to multiply the startIndex of string.Substring
            //      by 2. To get to the startIndex of a byte, we must substract by multiples of 2.
            // Item2 of ByteSpecification's dictionary value represents the number of bytes. Since two hex characters represent
            //      a byte, the number of characters to extract using string.Substring is Item2 * 2
            return _hasciiData.Substring(byteSpec[fieldName].Item1 * 2 - 2, byteSpec[fieldName].Item2 * 2);
        }

        public dynamic this[string category, string fieldName]
        {
            get
            {
                ByteSpecification spec = null;
                switch (category.ToLower())
                {
                    case "date":
                        spec = dateSpec;
                        goto case "cast";
                    case "constants":
                        spec = mathConstantsSpec;
                        goto case "cast";
                    case "secrets":
                        spec = secretsSpec;
                        goto case "cast";
                    case "cast":
                        string hascii_bytes = GetHasciiBytes(spec, fieldName); // Retrieve bytes from underlying string
                        return castop[fieldName](hascii_bytes); // Cast to appropriate type, according to mapping defined in CastOperation
                }
                return new ArgumentException();
            }

            set
            {
                switch (category.ToLower())
                {
                    case "secrets":
                        int insertLocation = secretsSpec[fieldName].Item1 * 2 - 2;
                        int maxCharFieldWidth = secretsSpec[fieldName].Item2 * 2; // Used for padding when the number of hex chars isn't even
                        string val = null;
                        switch (fieldName)
                        {
                            case "SecretValue":
                                val = String.Format("{0:X}", value).PadLeft(maxCharFieldWidth, '0'); // Convert value to hascii representation
                                goto case "EmplaceString";
                            case "SecretMessage":
                                val = HasciiParser.StringToHex(value, maxCharFieldWidth);
                                goto case "EmplaceString";
                            case "BigInt":
                                val = String.Format("{0:X}", value).PadLeft(maxCharFieldWidth, '0');
                                goto case "EmplaceString";
                            case "EmplaceString":
                                _hasciiData = _hasciiData.Remove(insertLocation, maxCharFieldWidth); // Remove the characters currently present
                                _hasciiData = _hasciiData.Insert(insertLocation, val ?? throw new InvalidOperationException());
                                Console.WriteLine(_hasciiData);
                                break;
                        }
                        break;
                    case "date":
                        throw new NotImplementedException();
                    case "constants":
                        throw new NotImplementedException();
                }
            }
        }
    }

    class Program
    {
        static void Main(string[] args)
        {
            /*
            string _year = "07E3"; // 2019  16-bit int, 0x07E3
            string _month = "0A"; // 10   8-bit int, 0x0A
            string _day = "02"; // 2   8-bit int, 0x02
            string _pi = "40490FD8"; // IEEE-754 Single Precision Float, 0x4049 0FD8
            string _eulers = "402DF854"; // IEEE-754 Single Precision Float, 0x402D F854
            string _secretValue = "80000000"; // 32-bit int, 0x80000000 (Decimal -2147483648)
            string _secretMsg = "48656C6C6F576F726C6421"; // ASCII string "HelloWorld!", 0x48656C6C6F576F726C6421 
            string _bigInt = "8000000000000000"; // 64-bit int, 0x8000 0000 0000 0000 (Decimal -9223372036854775808)
            */

            string hasciiData = "07E30A0240490FD8402df8548000000048656C6C6F576F726C64218000000000000000";

            PD0Format ensemble = new PD0Format(hasciiData);
            
            int recordYear = ensemble["date", "Year"];
            int recordMonth = ensemble["date", "Month"];
            int recordDay = ensemble["date", "Day"];
            Console.WriteLine(new DateTime(recordYear, recordMonth, recordDay));

            float Pi = ensemble["constants", "Pi"];
            float exp1 = ensemble["Constants", "EulersNumber"];
            Console.WriteLine($"Pi: {Pi}\nEuler's Number: {exp1}");

            int secretValue = ensemble["secrets", "SecretValue"];
            string secretMsg = ensemble["secrets", "SecretMessage"];
            long bigInt = ensemble["secrets", "BigInt"];
            Console.WriteLine($"Secret Value: {secretValue}\nSecret Msg: {secretMsg}\nbigInt: {bigInt}");

            //  Usage: Writing
            string defaultData = "0000000000000000000000000000000000000000000000000000000000000000000000";
            PD0Format defaultRecord = new PD0Format(defaultData);

            // 35791394 corresponds to 0x02222222 written as "02222222" in hascii
            defaultRecord["secrets", "SecretValue"] = 35791394;
            // "FooBarBaz" corresponds to 0x00 0046 6F6F 4261 7242 617A written as "0000466F6F42617242617A" in hascii
            defaultRecord["secrets", "SecretMessage"] = "FooBarBaz";
            // 1229782938247303441 corresponds to 0x1111 1111 1111 1111 written as "1111111111111111" in hascii
            defaultRecord["secrets", "BigInt"] = 1229782938247303441;

            // Original defaultData: "0000000000000000000000000000000000000000000000000000000000000000000000"
            // Modified defaultData: "000000000000000000000000022222220000466F6F42617242617A1111111111111111"
            Console.WriteLine(defaultRecord._hasciiData);

            Console.ReadLine(); // Prevent console from closing
        }
    }
}

Program Output

10/2/2019 00:00:00
Pi: 3.141592
Euler's Number: 2.718282
Secret Value: -2147483648
Secret Msg: HelloWorld!
bigInt: -9223372036854775808
0000000000000000000000000222222200000000000000000000000000000000000000
000000000000000000000000022222220000466F6F42617242617A0000000000000000
000000000000000000000000022222220000466F6F42617242617A1111111111111111
000000000000000000000000022222220000466F6F42617242617A1111111111111111
Minh Tran
  • 285
  • 1
  • 10