Compute and verify the <Stamp crc> checksum of IO-Link IODD files.
Zero dependencies. Plain JavaScript ES modules. Runs in Node, Bun, Deno and the browser.
Every IODD (IO Device Description) file ends with a block like this:
<Stamp crc="1462814215">
<Checker name="IODD-Checker V1.1.1" version="V1.1.1.0"/>
</Stamp>That crc is mandatory. Change one byte anywhere in the file and the stamp is
invalid — masters, engineering tools and the IODDfinder upload process will
reject it. Until now the only way to produce a correct one was to run a
closed-source Windows tool, because no open-source implementation of this
algorithm existed.
This is that implementation. It reproduces the official checksum byte-for-byte on every file tested, across four generations of the vendor checker (V1.1.1, V1.1.5, V1.1.13 and V2025.1).
Not affiliated with, or endorsed by, the IO-Link Community. "IO-Link" and "IODD" are trademarks of their respective owners.
bun add @calumk/ioddforge-checker
# or
npm install @calumk/ioddforge-checkerOr just copy src/crc.js into your project. It is a single
self-contained file with no imports.
# Check that stored CRCs are correct (exit code 1 on any mismatch)
ioddforge-crc verify ./my-device-IODD1.1.xml
ioddforge-crc verify ./iodd-folder/
# Print the CRC a file should have
ioddforge-crc compute ./my-device-IODD1.1.xml
# Rewrite the crc attribute in place
ioddforge-crc write ./iodd-folder/
# Full detail, including which checker stamped the file
ioddforge-crc info ./my-device-IODD1.1.xml$ ioddforge-crc verify ./examples/
ok examples/Balluff-BNI_IOL-800-000-Z036-20190215-IODD1.1.xml
ok examples/IO-Link-04-ExternalLangDevice-20180101-IODD1.1.xml
ok examples/IO-Link-04-ExternalLangDevice-20180101-IODD1.1-de.xml
ok examples/IO-Link-04-ExternalLangDevice-20180101-IODD1.1-zh.xml
4/4 valid
Add --json for machine-readable output. Add --main <file> if a translation
file's main IODD cannot be inferred from its filename.
write processes main IODDs before translations, so a whole directory
re-stamps correctly in one pass.
import {
computeStampCrc,
verifyStampCrc,
applyStampCrc,
isExternalTextDocument,
} from '@calumk/ioddforge-checker';
import { readFileSync, writeFileSync } from 'node:fs';
const bytes = new Uint8Array(readFileSync('device-IODD1.1.xml'));
// Is the stamp correct?
const { valid, stored, computed } = verifyStampCrc(bytes);
// Write a corrected stamp. Only the digits change — every other byte,
// including the BOM, indentation and line endings, is preserved exactly.
const { bytes: stamped, crc } = applyStampCrc(bytes);
writeFileSync('device-IODD1.1.xml', stamped);Translation files (<ExternalTextDocument> root) are chained to their main
IODD. Stamp the main file first, then pass its CRC:
const { crc: mainCrc } = applyStampCrc(mainBytes);
const { bytes: deStamped } = applyStampCrc(germanBytes, { mainIoddCrc: mainCrc });This means editing the main IODD invalidates every translation. That is by design — it binds the language pack to a specific revision of the device description.
| Function | Description |
|---|---|
crc32(bytes) |
Plain CRC-32/ISO-HDLC (same as zlib/PNG). |
new Crc32() |
Streaming variant: .update(bytes), .value, .reset(). |
computeStampCrc(bytes, { mainIoddCrc }) |
The IODD stamp CRC. Throws if the file has no stamp. |
verifyStampCrc(bytes, opts) |
{ valid, stored, computed }. |
applyStampCrc(bytes, opts) |
{ bytes, crc, previous }. Byte-preserving rewrite. |
findStampCrc(bytes) |
Locate the attribute: { valueStart, valueEnd, value }. |
isExternalTextDocument(bytes) |
Does this file need a mainIoddCrc? |
readChecker(bytes) |
{ name, version } of the tool that last stamped the file. |
All functions take and return Uint8Array — never a decoded string. See
the gotchas below for why.
From IODD Specification 10.012, in full:
- Use CRC-32 as defined in ITU-T V.42 §8.1.1.6.2 / ISO/IEC 13239:2002.
This is exactly the ordinary zlib/PNG CRC-32: polynomial
0x04C11DB7(reflected0xEDB88320), init0xFFFFFFFF, reflect in and out, final XOR0xFFFFFFFF. - Read the file in binary mode.
- Feed bytes into the CRC up to and including the literal string
<Stamp crc=". - Skip the attribute's digits entirely.
- Resume at the closing
"and hash through to end of file. - If — and only if — the root element is
<ExternalTextDocument>, then append the ASCII decimal digits (no leading zeroes) of the main IODD's CRC. - The result is an unsigned 32-bit integer, written in decimal.
Each of these cost a full round of brute-force searching to discover:
- Do not normalise line endings. Real IODDs are CRLF. Converting to LF silently breaks the checksum.
- Do not strip the UTF-8 BOM. It is part of the hashed content. Vendor files frequently have one.
- Do not add or trim a trailing newline. Some files have one, some don't; both are hashed as-is.
- Do not round-trip through an XML parser before hashing. Any reserialisation — attribute order, self-closing tags, entity escaping, whitespace — changes the bytes.
- Only
ExternalTextDocumentgets the appended CRC.IODevice,IODDStandardDefinitionsandIODDStandardUnitDefinitionsdo not. - Non-ASCII content is a red herring. A file with 217 CJK characters hashes correctly with no special handling, because everything is treated as opaque UTF-8 bytes.
Validated against 40 officially stamped documents:
| Source | Count |
|---|---|
| IO Device Description Guideline examples | 24 |
| Common Profile examples | 4 |
IODD-StandardDefinitions1.1.xml + 9 language variants |
10 |
IODD-StandardUnitDefinitions1.1.xml |
1 |
| Balluff BNI IOL-800-000-Z036 (real vendor IODD) | 1 |
Of these, 11 are ExternalTextDocument translations exercising the chained-CRC
path.
Every file passes three checks:
- the officially stamped value is reproduced exactly;
- re-stamping the file is byte-identical to the original;
- flipping a single bit anywhere is detected.
Stamps in the corpus were produced by checker versions V1.1.1, V1.1.5, V1.1.13 and V2025.1 — the algorithm has been stable across all of them.
bun testThe unit tests are self-contained. The corpus test is skipped unless you supply your own copy of the reference files, which are copyright IO-Link Community and are not redistributed here:
IODD_CORPUS=/path/to/iodd-files bun testGet suitable files from the IO Device Description Guideline example package, or download any vendor IODD from ioddfinder.io-link.com.
A widely-cited Stack Overflow answer claims the shipped IODD-Checker uses a different algorithm from the one in the specification, and that the documented procedure cannot be made to work.
That is not true for the CRC. The specification text is exactly accurate — every word of it. The reason it looks wrong is that it is easy to violate one of the gotchas above (usually the BOM or the line endings) without realising, and the resulting mismatch gets blamed on the spec rather than on the reader.
A fuller explanation of the algorithm is in
docs/how-the-iodd-crc-works.md.
This started as one piece of a larger project: a free, browser-based IODD editor to replace the proprietary Windows-only tooling that currently gates IO-Link device development. Producing a valid file is pointless if you cannot stamp it, so the CRC had to be solved first.
It turned out to be interesting enough — and useful enough on its own — to publish separately.
MIT © Calum Knott