From ffeb73ab7cc2d6bad70607bb728e37586c5d21b7 Mon Sep 17 00:00:00 2001 From: Matt Nadareski Date: Sat, 5 Nov 2022 23:29:04 -0700 Subject: [PATCH] Add proof-of-concept MS-DOS builder --- .../BurnOutSharp.Builder.csproj | 27 ++ BurnOutSharp.Builder/Extensions.cs | 276 ++++++++++++++++ BurnOutSharp.Builder/MSDOS.cs | 309 ++++++++++++++++++ BurnOutSharp.Models/MSDOS/ExecutableHeader.cs | 2 +- BurnOutSharp.sln | 8 +- 5 files changed, 620 insertions(+), 2 deletions(-) create mode 100644 BurnOutSharp.Builder/BurnOutSharp.Builder.csproj create mode 100644 BurnOutSharp.Builder/Extensions.cs create mode 100644 BurnOutSharp.Builder/MSDOS.cs diff --git a/BurnOutSharp.Builder/BurnOutSharp.Builder.csproj b/BurnOutSharp.Builder/BurnOutSharp.Builder.csproj new file mode 100644 index 00000000..681fc8a2 --- /dev/null +++ b/BurnOutSharp.Builder/BurnOutSharp.Builder.csproj @@ -0,0 +1,27 @@ + + + + netstandard2.0 + BurnOutSharp.Executable + BurnOutSharp.Executable + Matt Nadareski;Gernot Knippen + BurnOutSharp + Copyright (c)2005-2010 Gernot Knippen, Copyright (c)2018-2022 Matt Nadareski + LICENSE.txt + https://github.com/mnadareski/BurnOutSharp + 2.3.4 + 2.3.4 + 2.3.4 + true + true + + + + true + + + + + + + diff --git a/BurnOutSharp.Builder/Extensions.cs b/BurnOutSharp.Builder/Extensions.cs new file mode 100644 index 00000000..d0416883 --- /dev/null +++ b/BurnOutSharp.Builder/Extensions.cs @@ -0,0 +1,276 @@ +using System; +using System.Collections.Generic; +using System.IO; +using System.Linq; +using System.Text; + +namespace BurnOutSharp.Builder +{ + internal static class Extensions + { + #region Byte Arrays + + /// + /// Read a byte and increment the pointer to an array + /// + public static byte ReadByte(this byte[] content, ref int offset) + { + return content[offset++]; + } + + /// + /// Read a byte array and increment the pointer to an array + /// + public static byte[] ReadBytes(this byte[] content, ref int offset, int count) + { + byte[] buffer = new byte[count]; + Array.Copy(content, offset, buffer, 0, Math.Min(count, content.Length - offset)); + offset += count; + return buffer; + } + + /// + /// Read a char and increment the pointer to an array + /// + public static char ReadChar(this byte[] content, ref int offset) + { + return (char)content[offset++]; + } + + /// + /// Read a character array and increment the pointer to an array + /// + public static char[] ReadChars(this byte[] content, ref int offset, int count) => content.ReadChars(ref offset, count, Encoding.Default); + + /// + /// Read a character array and increment the pointer to an array + /// + public static char[] ReadChars(this byte[] content, ref int offset, int count, Encoding encoding) + { + // TODO: Fix the code below to make it work with byte arrays and not streams + return null; + + // byte[] buffer = new byte[count]; + // stream.Read(buffer, 0, count); + // return encoding.GetString(buffer).ToCharArray(); + } + + /// + /// Read a short and increment the pointer to an array + /// + public static short ReadInt16(this byte[] content, ref int offset) + { + short value = BitConverter.ToInt16(content, offset); + offset += 2; + return value; + } + + /// + /// Read a ushort and increment the pointer to an array + /// + public static ushort ReadUInt16(this byte[] content, ref int offset) + { + ushort value = BitConverter.ToUInt16(content, offset); + offset += 2; + return value; + } + + /// + /// Read a int and increment the pointer to an array + /// + public static int ReadInt32(this byte[] content, ref int offset) + { + int value = BitConverter.ToInt32(content, offset); + offset += 4; + return value; + } + + /// + /// Read a uint and increment the pointer to an array + /// + public static uint ReadUInt32(this byte[] content, ref int offset) + { + uint value = BitConverter.ToUInt32(content, offset); + offset += 4; + return value; + } + + /// + /// Read a long and increment the pointer to an array + /// + public static long ReadInt64(this byte[] content, ref int offset) + { + long value = BitConverter.ToInt64(content, offset); + offset += 8; + return value; + } + + /// + /// Read a ulong and increment the pointer to an array + /// + public static ulong ReadUInt64(this byte[] content, ref int offset) + { + ulong value = BitConverter.ToUInt64(content, offset); + offset += 8; + return value; + } + + /// + /// Read a null-terminated string from the stream + /// + public static string ReadString(this byte[] content, ref int offset) => content.ReadString(ref offset, Encoding.Default); + + /// + /// Read a null-terminated string from the stream + /// + public static string ReadString(this byte[] content, ref int offset, Encoding encoding) + { + byte[] nullTerminator = encoding.GetBytes(new char[] { '\0' }); + int charWidth = nullTerminator.Length; + + List keyChars = new List(); + while (BitConverter.ToUInt16(content, offset) != 0x0000) + { + keyChars.Add(encoding.GetChars(content, offset, charWidth)[0]); offset += charWidth; + } + offset += 2; + + return new string(keyChars.ToArray()); + } + + #endregion + + #region Streams + + /// + /// Read a byte from the stream + /// + public static byte ReadByteValue(this Stream stream) + { + byte[] buffer = new byte[1]; + stream.Read(buffer, 0, 1); + return buffer[0]; + } + + /// + /// Read a byte array from the stream + /// + public static byte[] ReadBytes(this Stream stream, int count) + { + byte[] buffer = new byte[count]; + stream.Read(buffer, 0, count); + return buffer; + } + + /// + /// Read a character from the stream + /// + public static char ReadChar(this Stream stream) + { + byte[] buffer = new byte[1]; + stream.Read(buffer, 0, 1); + return (char)buffer[0]; + } + + /// + /// Read a character array from the stream + /// + public static char[] ReadChars(this Stream stream, int count) => stream.ReadChars(count, Encoding.Default); + + /// + /// Read a character array from the stream + /// + public static char[] ReadChars(this Stream stream, int count, Encoding encoding) + { + byte[] buffer = new byte[count]; + stream.Read(buffer, 0, count); + return encoding.GetString(buffer).ToCharArray(); + } + + /// + /// Read a short from the stream + /// + public static short ReadInt16(this Stream stream) + { + byte[] buffer = new byte[2]; + stream.Read(buffer, 0, 2); + return BitConverter.ToInt16(buffer, 0); + } + + /// + /// Read a ushort from the stream + /// + public static ushort ReadUInt16(this Stream stream) + { + byte[] buffer = new byte[2]; + stream.Read(buffer, 0, 2); + return BitConverter.ToUInt16(buffer, 0); + } + + /// + /// Read an int from the stream + /// + public static int ReadInt32(this Stream stream) + { + byte[] buffer = new byte[4]; + stream.Read(buffer, 0, 4); + return BitConverter.ToInt32(buffer, 0); + } + + /// + /// Read a uint from the stream + /// + public static uint ReadUInt32(this Stream stream) + { + byte[] buffer = new byte[4]; + stream.Read(buffer, 0, 4); + return BitConverter.ToUInt32(buffer, 0); + } + + /// + /// Read a long from the stream + /// + public static long ReadInt64(this Stream stream) + { + byte[] buffer = new byte[8]; + stream.Read(buffer, 0, 8); + return BitConverter.ToInt64(buffer, 0); + } + + /// + /// Read a ulong from the stream + /// + public static ulong ReadUInt64(this Stream stream) + { + byte[] buffer = new byte[8]; + stream.Read(buffer, 0, 8); + return BitConverter.ToUInt64(buffer, 0); + } + + /// + /// Read a null-terminated string from the stream + /// + public static string ReadString(this Stream stream) => stream.ReadString(Encoding.Default); + + /// + /// Read a null-terminated string from the stream + /// + public static string ReadString(this Stream stream, Encoding encoding) + { + byte[] nullTerminator = encoding.GetBytes(new char[] { '\0' }); + int charWidth = nullTerminator.Length; + + List tempBuffer = new List(); + + byte[] buffer = new byte[charWidth]; + while (stream.Read(buffer, 0, charWidth) != 0 && buffer.SequenceEqual(nullTerminator)) + { + tempBuffer.AddRange(buffer); + } + + return encoding.GetString(tempBuffer.ToArray()); + } + + #endregion + } +} \ No newline at end of file diff --git a/BurnOutSharp.Builder/MSDOS.cs b/BurnOutSharp.Builder/MSDOS.cs new file mode 100644 index 00000000..969cdd45 --- /dev/null +++ b/BurnOutSharp.Builder/MSDOS.cs @@ -0,0 +1,309 @@ +using System.IO; +using BurnOutSharp.Models.MSDOS; + +namespace BurnOutSharp.Builder +{ + // TODO: Make Stream Data rely on Byte Data + public static class MSDOS + { + #region Byte Data + + /// + /// Parse a byte array into an MS-DOS executable + /// + /// Byte array to parse + /// Offset into the byte array + /// Filled executable on success, null on error + public static Executable ParseExecutable(byte[] data, int offset) + { + // If the data is invalid + if (data == null) + return null; + + // If the offset is out of bounds + if (offset < 0 || offset >= data.Length) + return null; + + // Cache the current offset + int initialOffset = offset; + + // Create a new executable to fill + var executable = new Executable(); + + // Try to parse the executable header + var executableHeader = ParseExecutableHeader(data, offset); + if (executableHeader == null) + return null; + + // Set the executable header + executable.Header = executableHeader; + + // If the offset for the relocation table doesn't exist + int tableAddress = initialOffset + executableHeader.RelocationTableAddr; + if (tableAddress >= data.Length) + return executable; + + // Try to parse the relocation table + var relocationTable = ParseRelocationTable(data, tableAddress, executableHeader.RelocationItems); + if (relocationTable == null) + return null; + + // Set the relocation table + executable.RelocationTable = relocationTable; + + // Return the executable + return executable; + } + + /// + /// Parse a byte array into an MS-DOS executable header + /// + /// Byte array to parse + /// Offset into the byte array + /// Filled executable header on success, null on error + private static ExecutableHeader ParseExecutableHeader(byte[] data, int offset) + { + // If we don't have enough data + if (data.Length < 28) + return null; + + // If the offset means we don't have enough data + if (data.Length - offset < 28) + return null; + + // TODO: Use marshalling here instead of building + var header = new ExecutableHeader(); + + #region Standard Fields + + header.Magic = new char[2]; + for (int i = 0; i < header.Magic.Length; i++) + { + header.Magic[i] = (char)data[offset]; offset++; + } + if (header.Magic[0] != 'M' || header.Magic[1] != 'Z') + return null; + + header.LastPageBytes = data.ReadUInt16(ref offset); + header.Pages = data.ReadUInt16(ref offset); + header.RelocationItems = data.ReadUInt16(ref offset); + header.HeaderParagraphSize = data.ReadUInt16(ref offset); + header.MinimumExtraParagraphs = data.ReadUInt16(ref offset); + header.MaximumExtraParagraphs = data.ReadUInt16(ref offset); + header.InitialSSValue = data.ReadUInt16(ref offset); + header.InitialSPValue = data.ReadUInt16(ref offset); + header.Checksum = data.ReadUInt16(ref offset); + header.InitialIPValue = data.ReadUInt16(ref offset); + header.InitialCSValue = data.ReadUInt16(ref offset); + header.RelocationTableAddr = data.ReadUInt16(ref offset); + header.OverlayNumber = data.ReadUInt16(ref offset); + + #endregion + + // If we don't have enough data for PE extensions + if (offset >= data.Length || data.Length - offset < 36) + return header; + + #region PE Extensions + + header.Reserved1 = new ushort[4]; + for (int i = 0; i < header.Reserved1.Length; i++) + { + header.Reserved1[i] = data.ReadUInt16(ref offset); + } + header.OEMIdentifier = data.ReadUInt16(ref offset); + header.OEMInformation = data.ReadUInt16(ref offset); + header.Reserved2 = new ushort[10]; + for (int i = 0; i < header.Reserved1.Length; i++) + { + header.Reserved2[i] = data.ReadUInt16(ref offset); + } + header.NewExeHeaderAddr = data.ReadUInt32(ref offset); + + #endregion + + return header; + } + + /// + /// Parse a byte array into a relocation table + /// + /// Byte array to parse + /// Offset into the byte array + /// Filled executable header on success, null on error + private static RelocationEntry[] ParseRelocationTable(byte[] data, int offset, int count) + { + // If we don't have enough data + if (data.Length < (count * 4)) + return null; + + // If the offset means we don't have enough data + if (data.Length - offset < (count * 4)) + return null; + + // TODO: Use marshalling here instead of building + var relocationTable = new RelocationEntry[count]; + + for (int i = 0; i < count; i++) + { + var entry = new RelocationEntry(); + entry.Offset = data.ReadUInt16(ref offset); + entry.Segment = data.ReadUInt16(ref offset); + relocationTable[i] = entry; + } + + return relocationTable; + } + + #endregion + + #region Stream Data + + /// + /// Parse a byte array into an MS-DOS executable + /// + /// Stream to parse + /// Filled executable on success, null on error + public static Executable ParseExecutable(Stream data) + { + // If the data is invalid + if (data == null) + return null; + + // If the offset is out of bounds + if (data.Position < 0 || data.Position >= data.Length) + return null; + + // Cache the current offset + int initialOffset = (int)data.Position; + + // Create a new executable to fill + var executable = new Executable(); + + // Try to parse the executable header + var executableHeader = ParseExecutableHeader(data); + if (executableHeader == null) + return null; + + // Set the executable header + executable.Header = executableHeader; + + // If the offset for the relocation table doesn't exist + int tableAddress = initialOffset + executableHeader.RelocationTableAddr; + if (tableAddress >= data.Length) + return executable; + + // Try to parse the relocation table + data.Seek(tableAddress, SeekOrigin.Begin); + var relocationTable = ParseRelocationTable(data, executableHeader.RelocationItems); + if (relocationTable == null) + return null; + + // Set the relocation table + executable.RelocationTable = relocationTable; + + // Return the executable + return executable; + } + + /// + /// Parse a byte array into an MS-DOS executable header + /// + /// Stream to parse + /// Filled executable header on success, null on error + private static ExecutableHeader ParseExecutableHeader(Stream data) + { + // If we don't have enough data + if (data.Length < 28) + return null; + + // If the offset means we don't have enough data + if (data.Length - data.Position < 28) + return null; + + // TODO: Use marshalling here instead of building + var header = new ExecutableHeader(); + + #region Standard Fields + + header.Magic = new char[2]; + for (int i = 0; i < header.Magic.Length; i++) + { + header.Magic[i] = data.ReadChar(); + } + if (header.Magic[0] != 'M' || header.Magic[1] != 'Z') + return null; + + header.LastPageBytes = data.ReadUInt16(); + header.Pages = data.ReadUInt16(); + header.RelocationItems = data.ReadUInt16(); + header.HeaderParagraphSize = data.ReadUInt16(); + header.MinimumExtraParagraphs = data.ReadUInt16(); + header.MaximumExtraParagraphs = data.ReadUInt16(); + header.InitialSSValue = data.ReadUInt16(); + header.InitialSPValue = data.ReadUInt16(); + header.Checksum = data.ReadUInt16(); + header.InitialIPValue = data.ReadUInt16(); + header.InitialCSValue = data.ReadUInt16(); + header.RelocationTableAddr = data.ReadUInt16(); + header.OverlayNumber = data.ReadUInt16(); + + #endregion + + // If we don't have enough data for PE extensions + if (data.Position >= data.Length || data.Length - data.Position < 36) + return header; + + #region PE Extensions + + header.Reserved1 = new ushort[4]; + for (int i = 0; i < header.Reserved1.Length; i++) + { + header.Reserved1[i] = data.ReadUInt16(); + } + header.OEMIdentifier = data.ReadUInt16(); + header.OEMInformation = data.ReadUInt16(); + header.Reserved2 = new ushort[10]; + for (int i = 0; i < header.Reserved1.Length; i++) + { + header.Reserved2[i] = data.ReadUInt16(); + } + header.NewExeHeaderAddr = data.ReadUInt32(); + + #endregion + + return header; + } + + /// + /// Parse a byte array into a relocation table + /// + /// Stream to parse + /// Filled executable header on success, null on error + private static RelocationEntry[] ParseRelocationTable(Stream data, int count) + { + // If we don't have enough data + if (data.Length < (count * 4)) + return null; + + // If the offset means we don't have enough data + if (data.Length - data.Position < (count * 4)) + return null; + + // TODO: Use marshalling here instead of building + var relocationTable = new RelocationEntry[count]; + + for (int i = 0; i < count; i++) + { + var entry = new RelocationEntry(); + entry.Offset = data.ReadUInt16(); + entry.Segment = data.ReadUInt16(); + relocationTable[i] = entry; + } + + return relocationTable; + } + + #endregion + } +} \ No newline at end of file diff --git a/BurnOutSharp.Models/MSDOS/ExecutableHeader.cs b/BurnOutSharp.Models/MSDOS/ExecutableHeader.cs index bbdbd37d..fdb56562 100644 --- a/BurnOutSharp.Models/MSDOS/ExecutableHeader.cs +++ b/BurnOutSharp.Models/MSDOS/ExecutableHeader.cs @@ -123,7 +123,7 @@ namespace BurnOutSharp.Models.MSDOS /// /// Starting address of the PE header /// - public int NewExeHeaderAddr; + public uint NewExeHeaderAddr; #endregion } diff --git a/BurnOutSharp.sln b/BurnOutSharp.sln index e1dc5e0a..05dd7a56 100644 --- a/BurnOutSharp.sln +++ b/BurnOutSharp.sln @@ -22,7 +22,9 @@ Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "WixToolset.Dtf.Compression. EndProject Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "WixToolset.Dtf.Compression", "Dtf\src\WixToolset.Dtf.Compression\WixToolset.Dtf.Compression.csproj", "{3065BE52-CB56-4557-80CD-E3AC57C242F2}" EndProject -Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "BurnOutSharp.Models", "BurnOutSharp.Models\BurnOutSharp.Models.csproj", "{44939210-BDC2-4250-BC0F-0AB8F316F09D}" +Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "BurnOutSharp.Models", "BurnOutSharp.Models\BurnOutSharp.Models.csproj", "{44939210-BDC2-4250-BC0F-0AB8F316F09D}" +EndProject +Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "BurnOutSharp.Builder", "BurnOutSharp.Builder\BurnOutSharp.Builder.csproj", "{7577733A-CC8D-4E7C-8B6D-FFC7EC1B3D07}" EndProject Global GlobalSection(SolutionConfigurationPlatforms) = preSolution @@ -58,6 +60,10 @@ Global {44939210-BDC2-4250-BC0F-0AB8F316F09D}.Debug|Any CPU.Build.0 = Debug|Any CPU {44939210-BDC2-4250-BC0F-0AB8F316F09D}.Release|Any CPU.ActiveCfg = Release|Any CPU {44939210-BDC2-4250-BC0F-0AB8F316F09D}.Release|Any CPU.Build.0 = Release|Any CPU + {7577733A-CC8D-4E7C-8B6D-FFC7EC1B3D07}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {7577733A-CC8D-4E7C-8B6D-FFC7EC1B3D07}.Debug|Any CPU.Build.0 = Debug|Any CPU + {7577733A-CC8D-4E7C-8B6D-FFC7EC1B3D07}.Release|Any CPU.ActiveCfg = Release|Any CPU + {7577733A-CC8D-4E7C-8B6D-FFC7EC1B3D07}.Release|Any CPU.Build.0 = Release|Any CPU EndGlobalSection GlobalSection(SolutionProperties) = preSolution HideSolutionNode = FALSE