Skip to content

Instantly share code, notes, and snippets.

@barncastle
Last active February 6, 2023 07:54
Show Gist options
  • Star 0 You must be signed in to star a gist
  • Fork 0 You must be signed in to fork a gist
  • Save barncastle/4277ac44aa47bf8c4389c7df0d160e56 to your computer and use it in GitHub Desktop.
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();
}
}
@barncastle
Copy link
Author

barncastle commented Feb 1, 2023

Compile the above code and either; drag and drop a file on to it or run using command line e.g. .\MultiSimConverter.exe "C:\Design1.ms14". This will generate a new file with the same location and name as the source file but an alternate extension. Be aware this will overwrite any existing file of the same name!

  • .ms14 will output .xml
  • .ewprj will output .xml
  • .xml will output either .ms14 or .ewprj depending on the format

NOTE: IMPLODE.DLL is required and must be situtated next to the app. This can be sourced here or here.

C-Style Structure:

struct CompressedElectronicsWorkbenchXML
{
   char signature[]; // "MSMCompressedElectronicsWorkbenchXML" or "CompressedElectronicsWorkbenchXML"
   BlockInfo header; // dwUncompressedSize stores the total uncompressed size. dwCompressedSize is 0
   Block blocks[x];  // the uncompressed file is split into 900k byte blocks each of which is then compressed
}

struct Block
{
    BlockInfo header;
    uint8_t compressedData[header.dwCompressedSize]; // PKWARE DLC Imploded
}

struct BlockInfo
{
    uint32_t dwUncompressedSize;
    uint32_t dwCompressedSize;
}

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