Skip to content

Instantly share code, notes, and snippets.

@barncastle
Last active April 20, 2026 08:13
Show Gist options
  • Select an option

  • Save barncastle/4277ac44aa47bf8c4389c7df0d160e56 to your computer and use it in GitHub Desktop.

Select an option

Save barncastle/4277ac44aa47bf8c4389c7df0d160e56 to your computer and use it in GitHub Desktop.
A tool to convert MultiSim project files between their uncompressed XML format and their compressed MS14/EWPRJ format
///
/// This code is licensed under the terms of the MIT license
///
using System.Runtime.CompilerServices;
using System.Runtime.InteropServices;
using System.Text;
using System.Text.RegularExpressions;
namespace MultiSimConverter
{
internal partial class Program
{
private const int BlockInfoSize = 8;
private const int MaxBlockSize = 900000;
[UnmanagedFunctionPointer(CallingConvention.Cdecl)]
private unsafe delegate uint ProcessBufferFunc(byte* buf, uint* size, PKParam* param);
[LibraryImport("IMPLODE.DLL", EntryPoint = "implode")]
private static unsafe partial int Implode(
ProcessBufferFunc read_func,
ProcessBufferFunc write_func,
byte* bWorkBuff,
ref PKParam param,
ref uint dwType,
ref uint dwImplSize);
[LibraryImport("IMPLODE.DLL", EntryPoint = "explode")]
private static unsafe partial int Explode(
ProcessBufferFunc read_func,
ProcessBufferFunc write_func,
byte* bWorkBuff,
ref PKParam param);
static void Main(string[] args)
{
static void ErrorAndExit(string message)
{
Console.WriteLine(message);
Environment.Exit(0);
}
if (args is not { Length: >= 1 })
ErrorAndExit("No file provided.");
if (!File.Exists(args[0]))
ErrorAndExit("Provided file not found.");
if (!File.Exists("IMPLODE.DLL"))
ErrorAndExit("IMPLODE.DLL not found.");
var fileName = args[0];
var fileData = File.ReadAllBytes(fileName);
// extract the signature from the first 0x80 bytes
var temp = Encoding.UTF8.GetString(fileData.AsSpan(0, 0x80));
var signature = SignatureRegex().Match(temp);
FileStream stream;
switch (signature.Value)
{
case "MSMCompressedElectronicsWorkbenchXML":
stream = File.Create(Path.ChangeExtension(fileName, ".xml"));
Decompress(fileData.AsSpan(36), stream);
break;
case "CompressedElectronicsWorkbenchXML":
stream = File.Create(Path.ChangeExtension(fileName, ".xml"));
Decompress(fileData.AsSpan(33), stream);
break;
case "MSMElectronicsWorkbench":
stream = File.Create(Path.ChangeExtension(fileName, ".ms14"));
Compress(fileData, stream, "MSMCompressedElectronicsWorkbenchXML"u8);
break;
case "ElectronicsWorkbench":
stream = File.Create(Path.ChangeExtension(fileName, ".ewprj"));
Compress(fileData, stream, "CompressedElectronicsWorkbenchXML"u8);
break;
default:
ErrorAndExit("Provided file is invalid.");
return;
}
if (stream.Length == 0)
ErrorAndExit("Unable to convert file.");
stream.Flush(true);
stream.Close();
stream.Dispose();
ErrorAndExit($"Generated '{Path.GetFileName(stream.Name)}'.");
}
private unsafe static void Compress(Span<byte> input, Stream stream, ReadOnlySpan<byte> signature)
{
var outputBuffer = new byte[input.Length + 0x1000];
var compressBuffer = new byte[0x8DD8];
uint bAscii = 1;
uint dwDictSize = 0x40 << 6;
int bytesRead = 0, bytesWritten = BlockInfoSize;
while (bytesRead < input.Length)
{
fixed (byte* pCompressBuffer = &compressBuffer[0])
fixed (byte* pInput = &input[bytesRead])
fixed (byte* pOutput = &outputBuffer[bytesWritten])
{
var blockInfo = &*(BlockInfo*)pOutput;
var blockSize = Math.Min(input.Length - bytesRead, MaxBlockSize);
var param = new PKParam
{
pCompressedData = pInput,
pDecompressedData = pOutput + BlockInfoSize,
dwMaxRead = (uint)blockSize,
dwMaxWrite = (uint)(input.Length - bytesRead)
};
// compress this block
if (Implode(ReadBytes, WriteBytes, pCompressBuffer, ref param, ref bAscii, ref dwDictSize) != 0)
return;
// update the block info
blockInfo->dwUncompressedSize = param.dwReadPos;
blockInfo->dwCompressedSize = param.dwWritePos;
bytesRead += (int)param.dwReadPos;
bytesWritten += (int)param.dwWritePos + BlockInfoSize;
}
}
// prepend the total decompressed size
Unsafe.WriteUnaligned(ref outputBuffer[0], (uint)bytesRead);
// write the signature and output buffer to the output
stream.Write(signature);
stream.Write(outputBuffer, 0, bytesWritten);
}
private unsafe static void Decompress(Span<byte> input, Stream stream)
{
// read the total uncompressed size
var uncompressedSize = Unsafe.ReadUnaligned<uint>(ref input[0]);
var outputBuffer = new byte[uncompressedSize];
var compressBuffer = new byte[0x8DD8];
for (int bytesRead = BlockInfoSize, bytesWritten = 0; bytesRead < input.Length;)
{
fixed (byte* pCompressBuffer = &compressBuffer[0])
fixed (byte* pInput = &input[bytesRead])
fixed (byte* pOutput = &outputBuffer[bytesWritten])
{
// read the block header
var blockInfo = *(BlockInfo*)pInput;
var param = new PKParam
{
pCompressedData = pInput + BlockInfoSize,
pDecompressedData = pOutput,
dwMaxRead = blockInfo.dwCompressedSize,
dwMaxWrite = blockInfo.dwUncompressedSize,
};
// decompress this block
if (Explode(ReadBytes, WriteBytes, pCompressBuffer, ref param) != 0)
return;
bytesRead += (int)param.dwReadPos + BlockInfoSize;
bytesWritten += (int)param.dwWritePos;
}
}
stream.Write(outputBuffer);
}
private static unsafe uint ReadBytes(byte* buf, uint* size, PKParam* param)
{
var bytesRead = Math.Min(param->dwMaxRead - param->dwReadPos, *size);
Unsafe.CopyBlockUnaligned(buf, &param->pCompressedData[param->dwReadPos], bytesRead);
param->dwReadPos += bytesRead;
return bytesRead;
}
private static unsafe uint WriteBytes(byte* buf, uint* size, PKParam* param)
{
if (param->dwWritePos + *size <= param->dwMaxWrite)
Unsafe.CopyBlockUnaligned(&param->pDecompressedData[param->dwWritePos], buf, *size);
param->dwWritePos += *size;
return 0;
}
[StructLayout(LayoutKind.Sequential)]
private struct BlockInfo
{
public uint dwUncompressedSize;
public uint dwCompressedSize;
}
[StructLayout(LayoutKind.Sequential)]
private unsafe struct PKParam
{
public byte* pCompressedData;
public uint dwReadPos;
public byte* pDecompressedData;
public uint dwWritePos;
public uint dwMaxRead;
public uint dwMaxWrite;
}
[GeneratedRegex("(MSM)?(Compressed)?ElectronicsWorkbench(XML)?")]
private static partial Regex SignatureRegex();
}
}
@cjohnsonoem
Copy link
Copy Markdown

cjohnsonoem commented Apr 15, 2026

If anyone want's to get this working in .net framework 4.7.2 Below is the modified code for C# 7.3

using System;
using System.IO;
using System.Runtime.CompilerServices;
using System.Runtime.InteropServices;
using System.Text;
using System.Text.RegularExpressions;

namespace MultiSimConverter
{
    internal class Program   // Changed from partial class (not needed here)
    {
        private const int BlockInfoSize = 8;
        private const int MaxBlockSize = 900000;

        [UnmanagedFunctionPointer(CallingConvention.Cdecl)]
        private unsafe delegate uint ProcessBufferFunc(byte* buf, uint* size, PKParam* param);

        [DllImport("IMPLODE.DLL", CallingConvention = CallingConvention.Cdecl, EntryPoint = "implode")]
        private static unsafe extern int Implode(
            ProcessBufferFunc read_func,
            ProcessBufferFunc write_func,
            byte* bWorkBuff,
            ref PKParam param,
            ref uint dwType,
            ref uint dwImplSize);

        [DllImport("IMPLODE.DLL", CallingConvention = CallingConvention.Cdecl, EntryPoint = "explode")]
        private static unsafe extern int Explode(
            ProcessBufferFunc read_func,
            ProcessBufferFunc write_func,
            byte* bWorkBuff,
            ref PKParam param);

        static void Main(string[] args)
        {
            if (args == null || args.Length < 1)
            {
                ErrorAndExit("No file provided.");
            }

            if (!File.Exists(args[0]))
            {
                ErrorAndExit("Provided file not found.");
            }

            if (!File.Exists("IMPLODE.DLL"))
            {
                ErrorAndExit("IMPLODE.DLL not found. Place the 32-bit IMPLODE.DLL in the same folder as the executable.");
            }

            var fileName = args[0];
            var fileData = File.ReadAllBytes(fileName);

            // Extract signature from the first 0x80 bytes
            var temp = Encoding.UTF8.GetString(fileData, 0, Math.Min(0x80, fileData.Length));
            var signatureMatch = SignatureRegex.Match(temp);
            var signature = signatureMatch.Value;

            FileStream stream = null;

            try
            {
                switch (signature)
                {
                    case "MSMCompressedElectronicsWorkbenchXML":
                        stream = File.Create(Path.ChangeExtension(fileName, ".xml"));
                        Decompress(fileData.AsSpan(36), stream);
                        break;

                    case "CompressedElectronicsWorkbenchXML":
                        stream = File.Create(Path.ChangeExtension(fileName, ".xml"));
                        Decompress(fileData.AsSpan(33), stream);
                        break;

                    case "MSMElectronicsWorkbench":
                        stream = File.Create(Path.ChangeExtension(fileName, ".ms14"));
                        Compress(fileData.AsSpan(), stream, "MSMCompressedElectronicsWorkbenchXML");
                        break;

                    case "ElectronicsWorkbench":
                        stream = File.Create(Path.ChangeExtension(fileName, ".ewprj"));
                        Compress(fileData.AsSpan(), stream, "CompressedElectronicsWorkbenchXML");
                        break;

                    default:
                        ErrorAndExit("Provided file is invalid or unsupported format.");
                        return;
                }

                if (stream.Length == 0)
                {
                    ErrorAndExit("Unable to convert file.");
                }

                stream.Flush(true);
                Console.WriteLine($"Success! Generated '{Path.GetFileName(stream.Name)}'.");
            }
            finally
            {
                stream?.Close();
                stream?.Dispose();
            }
        }

        private static void ErrorAndExit(string message)
        {
            Console.WriteLine(message);
            Environment.Exit(0);
        }

        private unsafe static void Compress(Span<byte> input, Stream stream, string signatureString)
        {
            var signature = Encoding.ASCII.GetBytes(signatureString);
            var outputBuffer = new byte[input.Length + 0x1000];
            var compressBuffer = new byte[0x8DD8];
            uint bAscii = 1;
            uint dwDictSize = 0x40 << 6;

            int bytesRead = 0;
            int bytesWritten = BlockInfoSize;

            while (bytesRead < input.Length)
            {
                fixed (byte* pCompressBuffer = compressBuffer)
                fixed (byte* pInput = &input[bytesRead])
                fixed (byte* pOutput = &outputBuffer[bytesWritten])
                {
                    var blockInfo = (BlockInfo*)pOutput;
                    var blockSize = Math.Min(input.Length - bytesRead, MaxBlockSize);

                    var param = new PKParam
                    {
                        pCompressedData = pInput,
                        pDecompressedData = pOutput + BlockInfoSize,
                        dwMaxRead = (uint)blockSize,
                        dwMaxWrite = (uint)(input.Length - bytesRead)
                    };

                    if (Implode(ReadBytes, WriteBytes, pCompressBuffer, ref param, ref bAscii, ref dwDictSize) != 0)
                    {
                        Console.WriteLine("Compression failed.");
                        return;
                    }

                    blockInfo->dwUncompressedSize = param.dwReadPos;
                    blockInfo->dwCompressedSize = param.dwWritePos;

                    bytesRead += (int)param.dwReadPos;
                    bytesWritten += (int)param.dwWritePos + BlockInfoSize;
                }
            }

            // Prepend total uncompressed size
            Unsafe.WriteUnaligned(ref outputBuffer[0], (uint)bytesRead);

            // Write signature + data
            stream.Write(signature, 0, signature.Length);
            stream.Write(outputBuffer, 0, bytesWritten);
        }

        private unsafe static void Decompress(Span<byte> input, Stream stream)
        {
            // Read total uncompressed size
            var uncompressedSize = Unsafe.ReadUnaligned<uint>(ref input[0]);
            if (uncompressedSize > 500_000_000) // safety limit
            {
                Console.WriteLine("Uncompressed size too large.");
                return;
            }

            var outputBuffer = new byte[uncompressedSize];
            var compressBuffer = new byte[0x8DD8];

            int bytesRead = BlockInfoSize;
            int bytesWritten = 0;

            while (bytesRead < input.Length)
            {
                fixed (byte* pCompressBuffer = compressBuffer)
                fixed (byte* pInput = &input[bytesRead])
                fixed (byte* pOutput = &outputBuffer[bytesWritten])
                {
                    var blockInfo = *(BlockInfo*)pInput;

                    var param = new PKParam
                    {
                        pCompressedData = pInput + BlockInfoSize,
                        pDecompressedData = pOutput,
                        dwMaxRead = blockInfo.dwCompressedSize,
                        dwMaxWrite = blockInfo.dwUncompressedSize
                    };

                    if (Explode(ReadBytes, WriteBytes, pCompressBuffer, ref param) != 0)
                    {
                        Console.WriteLine("Decompression failed.");
                        return;
                    }

                    bytesRead += (int)param.dwReadPos + BlockInfoSize;
                    bytesWritten += (int)param.dwWritePos;
                }
            }

            stream.Write(outputBuffer, 0, outputBuffer.Length);
        }

        private static unsafe uint ReadBytes(byte* buf, uint* size, PKParam* param)
        {
            var bytesToRead = Math.Min(param->dwMaxRead - param->dwReadPos, *size);
            Unsafe.CopyBlockUnaligned(buf, &param->pCompressedData[param->dwReadPos], bytesToRead);
            param->dwReadPos += bytesToRead;
            return bytesToRead;
        }

        private static unsafe uint WriteBytes(byte* buf, uint* size, PKParam* param)
        {
            if (param->dwWritePos + *size <= param->dwMaxWrite)
            {
                Unsafe.CopyBlockUnaligned(&param->pDecompressedData[param->dwWritePos], buf, *size);
            }
            param->dwWritePos += *size;
            return 0;
        }

        [StructLayout(LayoutKind.Sequential)]
        private struct BlockInfo
        {
            public uint dwUncompressedSize;
            public uint dwCompressedSize;
        }

        [StructLayout(LayoutKind.Sequential)]
        private unsafe struct PKParam
        {
            public byte* pCompressedData;
            public uint dwReadPos;
            public byte* pDecompressedData;
            public uint dwWritePos;
            public uint dwMaxRead;
            public uint dwMaxWrite;
        }

        private static readonly Regex SignatureRegex = new Regex(
            @"(MSM)?(Compressed)?ElectronicsWorkbench(XML)?",
            RegexOptions.Compiled);
    }
}

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment