Last active
February 6, 2023 07:54
A tool to convert MultiSim project files between their uncompressed XML format and their compressed MS14/EWPRJ format
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
/// | |
/// 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, ¶m->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(¶m->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(); | |
} | |
} |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
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 formatNOTE:
IMPLODE.DLL
is required and must be situtated next to the app. This can be sourced here or here.C-Style Structure: