https://github.com/cinderblock/electronics-workbench-decoder
Electronics Workbench (MultiSIM/Ultiboard) proprietary file format decoder
https://github.com/cinderblock/electronics-workbench-decoder
multisim national-instruments ultiboard
Last synced: 12 days ago
JSON representation
Electronics Workbench (MultiSIM/Ultiboard) proprietary file format decoder
- Host: GitHub
- URL: https://github.com/cinderblock/electronics-workbench-decoder
- Owner: cinderblock
- License: isc
- Created: 2021-08-07T22:28:46.000Z (almost 5 years ago)
- Default Branch: master
- Last Pushed: 2026-05-29T00:27:54.000Z (20 days ago)
- Last Synced: 2026-05-29T01:21:50.127Z (20 days ago)
- Topics: multisim, national-instruments, ultiboard
- Language: TypeScript
- Homepage:
- Size: 432 KB
- Stars: 5
- Watchers: 2
- Forks: 0
- Open Issues: 0
-
Metadata Files:
- Readme: README.md
- License: LICENSE
Awesome Lists containing this project
README
# Electronics Workbench Decoder and Encoder
Interop tools for Electronics Workbench / Multisim / Ultiboard binary
formats. The goal: get designs and part databases out of EW into
mainstream representations (and back, where the format permits).
- **`ewd`** — decode an EW file into JSON or XML, picking the right
decoder by reading the file's magic bytes.
- **`ewe`** — encode the text-form back into an EW binary the original
software can re-open. Currently supports compressed-XML; `.prj` Jet
database encode is deferred (see below).
## Supported formats
| Key | Extension(s) | Kind | Decode output |
| ---------- | ------------------ | ---------------- | ---------------- |
| `ewprj` | `.ewprj` | `compressed-xml` | `.xml` |
| `multisim` | `.ms10` … `.ms19` | `compressed-xml` | `.xml` |
| `mdb` | `.prj`, `.usr` | `mdb` | `.json` |
### compressed-xml (`ewprj`, `multisim`)
PKWare DCL Implode (ASCII literal mode, large dictionary) wrapped in a
small container: a magic-string header, a 64-bit LE total decompressed
length, then a sequence of `(decompressed_length: u32,
compressed_length: u32, pkware_implode_block)` sections. Multisim chunks
big files into 900 000-decompressed-byte sections; `ewe` mirrors that.
Decode and encode are both supported. A decode/encode/decode round-trip
on every sample tested produces byte-identical XML, and the resulting
files are accepted by `ewd`.
### mdb (`.prj`, `.usr`)
NI ships part libraries and project component databases as Microsoft
Access Jet 3 / Jet 4 files (despite the `.prj` / `.usr` extensions).
`ewd` reads them via [`mdb-reader`](https://www.npmjs.com/package/mdb-reader)
and emits a single JSON document with every table:
```json
{
"format": "mdb",
"source": "Stocked.prj",
"tables": {
"SYS_COMPONENT": {
"columns": [{ "name": "Component_ID", "type": "long" }, ...],
"rows": [{ "Component_ID": 17, "Component_Name": "BD9763FVM", ... }, ...]
},
...
}
}
```
Dates become ISO 8601 strings; binary blob fields (`ole` columns) become
`{ "_bytes": "base64", "value": "..." }` envelopes so the data survives a
JSON round-trip.
**Encode is a deliberately narrow byte-patch path, not a real Jet writer.**
Reimplementing Access's storage engine (page splits, B-tree index
maintenance, free-space tracking, `MSysObjects` invariants) is out of
scope for this project — `ewd`/`ewe` exist to move data **out** of
Electronics Workbench, not to clone Microsoft's database engine.
What `ewe` does on an `mdb` input today:
1. You decode with `ewd` to produce `.prj.json`.
2. You edit the JSON in your text editor (or with a script).
3. You run `ewe edited.json`. It diffs the edited JSON against the
original `.prj`, then for each changed cell where:
- the column is `text` or `memo`,
- the new value has the **same byte length** as the old,
- the old value appears **exactly once** in the file,
it overwrites the bytes in place. Anything that violates those
constraints is skipped, and `--verbose` prints the reason per cell.
This is enough for the typical vendor / price / MPN editing workflow
because those values are usually unique and you usually substitute one
same-length identifier for another. It is **not** a general-purpose
editor and we don't intend to grow it into one.
### If you want general-purpose `.prj` edit support
Not planned in this repo. The clean way to add it is a separate,
optional, Windows-only tool that drives Microsoft's own Access engine:
- **ACE OLE DB Provider** (`Microsoft.ACE.OLEDB.12.0` / `16.0`) — free
Redistributable from Microsoft, reads & writes both `.mdb` and `.accdb`.
- **Access ODBC driver** — same engine, ODBC interface. Pair with
[`node-odbc`](https://github.com/markdirish/node-odbc) (N-API native
addon) and issue plain SQL: `UPDATE SYS_USER_DATA SET USER_DEFINED_7 =
? WHERE Component_ID = ?`. The diff logic we already have for
edited-JSON → changed-cells would generate those statements directly.
- **Jet OLE DB 4.0** — older, **32-bit only**, still works for Jet 3
`.mdb` files like NI's `.prj`.
That path requires Windows + a driver install and so deliberately lives
outside this cross-platform repo's CI. The current byte-patch encode
covers the vendor/price editing case; for anything harder, route through
Access (or the ACE driver, or DAO/ADO) instead of expecting `ewe` to
grow into a full Jet writer.
## Format detection
`ewd` identifies the format by reading the magic bytes at the start of
the file, so the filename extension can be anything (renamed files, no
extension, `.dat`, etc.). It then dispatches to the right decoder.
`ewe` infers the format from the **output** filename's extension; pass
`--format ` to override for non-standard extensions. Out-of-range
Multisim versions (e.g. `.ms9` or future `.ms20+`) need an explicit
`--format multisim`.
## Install
### As a CLI (from npm)
```bash
npm i -g electronics-workbench-decoder # puts `ewd` and `ewe` on your PATH
# …or run without installing:
npx -p electronics-workbench-decoder ewd MyDesign.ms14
```
The published package is a self-contained bundle that runs on Node ≥ 18 —
you don't need bun to use it. Installed this way, invoke the tools directly
as `ewd` / `ewe` (the `bun run ewd` form below is for working from source).
### For development (from source)
Requires [bun](https://bun.sh) (≥ 1.0).
```bash
bun install
```
## Decode
```bash
bun run ewd --verbose ./samples/Temp.ewprj ./samples/Design1.ms14
bun run ewd --verbose ./samples/Stocked.prj # writes Stocked.prj.json
```
For each input, writes `.xml` (compressed-xml) or
`.json` (mdb) next to it.
Options:
- `-v`, `--verbose` — log per-section progress
- `-c`, `--concurrent` — decode multiple files in parallel
- positional args — files to decode
## Encode
### Compressed-XML (greenfield encode from `.xml`)
```bash
bun run ewe --verbose ./samples/Temp.ewprj.xml
# or with explicit output:
bun run ewe --output ./out.ewprj ./samples/Temp.ewprj.xml
# or force a format on a non-standard extension:
bun run ewe --format multisim --output ./out.dat ./samples/Design1.ms14.xml
```
By default, strips a trailing `.xml` from each input to derive the output
path. Format is inferred from the output extension (see the table above).
### MDB in-place edits from `.json`
```bash
# Decode first to produce the editable JSON.
bun run ewd ./samples/Stocked.prj
# Edit ./samples/Stocked.prj.json in your editor / script.
# Then re-encode: ewe diffs vs the original and writes a patched copy.
bun run ewe --verbose ./samples/Stocked.prj.json
# -> samples/Stocked.prj.patched.prj
```
`ewe` recognizes `.json` inputs and routes them through the in-place
edit path. The original `.prj` location is taken from the JSON's
`source` field (or `--source ` to override). The patched copy
goes to `.patched.prj` (or `--output ` to override).
Each `--verbose` run prints `N applied, M skipped` and lists every
skipped change with its reason. Skipped changes leave the file
untouched at that cell.
### Options
- `-v`, `--verbose` — log progress
- `-c`, `--concurrent` — encode multiple files in parallel
- `-o`, `--output ` — explicit output path (single-input only)
- `-s`, `--source ` — for MDB JSON inputs: override the source `.prj`
- `-f`, `--format ` — for compressed-XML: force a format (`ewprj` or `multisim`)
- positional args — XML or JSON files to encode
### Compression ratio caveat
`ewe` uses [`node-pkware`](https://github.com/cinderblock/node-pkware)'s
`implode` for compression. The repetition search isn't quite as
sophisticated as the original PKWare DCL implementation Multisim itself
uses — re-encoded files are roughly **2× the size** of the originals
(e.g. 9 KB `.ewprj` round-trips to 20 KB). The output is still valid
PKWare DCL Implode: round-trips are byte-identical on every sample
tested, and the resulting files are accepted by `ewd`.
This is a substantial improvement over the previous baseline (7-10×
bloat) thanks to fixes in the `v5-fix` branch of the
`cinderblock/node-pkware` fork — see `plans/jet-in-place-edit.md`
context and the fork's `v5-fix` branch for the specific bugs that
were patched.
## Programmatic API
The package also ships a library API (ESM + CJS, with TypeScript types) — the
same functions the CLIs are built on.
```ts
import {
decodeFile,
decodeBuffer,
encodeBuffer,
decodeMdbBuffer,
formatByKey,
} from 'electronics-workbench-decoder';
// Filename → decoded JS values (format detected from the file's magic bytes)
const result = await decodeFile('Design1.ms14');
if (result.kind === 'compressed-xml') {
console.log(result.xml.toString()); // the decoded XML
} else {
console.log(result.data.tables.SYS_COMPONENT.rows); // Jet DB as a JS object
}
// Buffer in, buffer out — no file I/O
const { xml } = decodeBuffer(ewprjBytes);
const ewprj = encodeBuffer(xml, formatByKey('ewprj')!);
const db = decodeMdbBuffer(prjBytes); // → { format, source, tables: {...} }
```
Exposed: `decodeFile`, `decodeBuffer`, `encodeBuffer`, `decodeMdbBuffer`, the
format registry (`FORMATS`, `formatByKey`, `formatForExtension`,
`detectFormatByHeader`, `detectFileFormat`), and the supporting types
(`EwbFormat`, `MdbJson`, `DecodeResult`, `DecodedXml`, …).
Decoding covers every supported format. Encoding via the API covers the
compressed-xml containers (`encodeBuffer`); `.prj` in-place editing remains
CLI-only (`ewe`).
### Memory
Everything operates in memory — a file is read whole, decoded/encoded whole,
then written. Peak RAM is on the order of the input plus the decoded output.
This is fine for the file sizes Electronics Workbench produces (KB to a few
MB). Streaming wouldn't help the `.prj`/`.usr` databases anyway (Jet is a
random-access format that `mdb-reader` must load fully), and for the
compressed-xml containers it would only cap RAM at one ~900 KB section — a
marginal win not worth the added complexity at these sizes.
## Tests
```bash
bun test
```
Covers the format registry (extension matching, header detection
including the Jet magic prefix with leading NUL bytes, on-disk
detection), encoder filename helpers, `mdb` value normalization
(`Date` → ISO, OLE blobs → base64), the public library API (buffer
round-trips, `decodeFile` against a temp file), and compressed-xml
round-trip integration (tiny `.ewprj`, tiny `.ms14`, multi-block
payload that spans more than one PKWare section, and an empty payload).
## Development
```bash
bun run dev:ewd --verbose ./samples/Temp.ewprj
bun run dev:ewe --verbose ./samples/Temp.ewprj.xml
```
Both use `bun --watch` for fast reload on source changes.
## Lint, format, and typecheck
```bash
bun run check # biome lint + format check (no changes)
bun run check:fix # apply all safe fixes
bun run format # format only
bun run typecheck # tsc --noEmit
```
CI on push/PR runs `check`, `typecheck`, and `test` on Ubuntu and Windows.
## Releasing
Publishing to npm is automated by GitHub Actions
(`.github/workflows/publish.yml`) using npm Trusted Publishing (OIDC):
no stored `NPM_TOKEN`, and each release gets a signed provenance
attestation.
To cut a release:
```bash
npm version patch # or `minor` / `major`; bumps package.json, commits, tags vX.Y.Z
git push --follow-tags # pushing the tag triggers the publish workflow
```
The `vX.Y.Z` tag triggers the workflow, which verifies the tag matches
`package.json`, runs the full gate (`check` + `typecheck` + `test` +
license collection + `build`), then `npm publish --provenance`.
Notes for maintainers:
- The npm package has a **Trusted Publisher** configured for this repo and
the `publish.yml` workflow. If you rename the workflow file or the repo,
update that entry on npmjs.com to match — otherwise the OIDC identity
won't match and publishing fails with a 404/permission error.
- The published artifact is a self-contained bundle. `dist/` and
`THIRD-PARTY-LICENSES.md` are generated at publish time (by the
`prepublishOnly` script) and are not committed.
- Runtime dependencies are bundled into `dist/`, so the published package
declares none. `THIRD-PARTY-LICENSES.md` carries their license texts.
## See also
- [barncastle's `MultiSimConverter`](https://gist.github.com/barncastle/4277ac44aa47bf8c4389c7df0d160e56)
— a separate, independent C# implementation that handles the same
compressed-xml container. It round-trips the `.ms14` / `.ewprj` container
to and from plain XML by
P/Invoking the original PKWare `IMPLODE.DLL` (so its compression matches
Multisim's exactly, avoiding the [ratio caveat](#compression-ratio-caveat)
above). It dispatches on the same magic strings `ewd` reads
(`MSMCompressedElectronicsWorkbenchXML`, `CompressedElectronicsWorkbenchXML`)
and uses the same 900 000-byte block size. It covers only the compressed-xml
container — not the Jet (`.prj` / `.usr`) part databases, and it doesn't
convert designs to other EDA formats.