Every IODD (IO Device Description) file carries a checksum near the end:
<Stamp crc="1462814215">
<Checker name="IODD-Checker V1.1.1" version="V1.1.1.0"/>
</Stamp>
It is mandatory. Change one byte anywhere in the file and the stamp no longer
matches, and the file will be rejected by engineering tools, by masters, and by
the IODDfinder upload process.
Despite the IO-Link ecosystem being nearly two decades old, there has been no
open-source implementation of this algorithm — the only way to produce a valid
stamp was to run the official closed-source, Windows-only IODD-Checker. This
post explains exactly how the checksum is computed.
Just want the code?
calumk
/
IODDForge_Checker
Compute and verify the <Stamp crc> checksum of IO-Link IODD files. Zero dependencies, works in Node, Bun and the browser.
IODDForge Checker
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…
AI Disclaimer
This repo, and the post you are about to read, was written by AI - it bares my name, but really is authored by Claud Opus 5.
The algorithm
The procedure is defined in IODD Specification 10.012, and the specification
text is precise and complete. In summary:
- Use CRC-32 as defined in ITU-T V.42 §8.1.1.6.2 / ISO/IEC 13239:2002.
- 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 the root element is
<ExternalTextDocument>, append the ASCII decimal digits of the main IODD's CRC. - The result is an unsigned 32-bit integer, written in decimal.
Each of those steps hides a detail worth spelling out.
It's just zlib CRC-32
The specification's reference to ITU-T V.42 sounds exotic, but that standard
describes the ordinary CRC-32 everyone already has: the same one used by zlib,
gzip and PNG. Formally, CRC-32/ISO-HDLC:
| Parameter | Value |
|---|---|
| Polynomial |
0x04C11DB7 (reflected: 0xEDB88320) |
| Initial value | 0xFFFFFFFF |
| Reflect in / out | yes / yes |
| Final XOR | 0xFFFFFFFF |
Check ("123456789") |
0xCBF43926 |
If a library gives you 0xCBF43926 for the string 123456789, it is the right
function. No custom polynomial is involved.
The self-reference trick
A checksum stored inside the file it protects is a circular definition: writing
the value changes the bytes, which changes the value.
The standard escape is to zero the field before hashing. IODD instead skips
it. The byte stream fed to the CRC is the file with a hole punched in it,
exactly where the digits live:
<?xml version="1.0" encoding="UTF-8"?>
<IODevice ...>
...
<Stamp crc="1462814215"><Checker .../></Stamp>
└────┬────┘
hashed │ hashed
◄───────────────►│◄──────────────────────►
skipped
</IODevice>
In code, that's two calls into a streaming CRC:
const stamp = findStampCrc(bytes); // locate the attribute value
const crc = new Crc32();
crc.update(bytes.subarray(0, stamp.valueStart)); // through `<Stamp crc="`
crc.update(bytes.subarray(stamp.valueEnd)); // from `"` to EOF
A useful consequence: the stamp's width doesn't matter. crc="", crc="0" and
crc="4294967295" all hash identically, so a freshly authored file with a
placeholder produces the same result as a correctly stamped one. Stamping is
idempotent.
"Binary mode" is the important phrase
Step 2 is where most independent implementations go wrong, because the
deviations are ones a language runtime makes on your behalf.
A UTF-8 BOM is part of the content. Many vendor IODDs begin with EF BB BF.
Reading the file as text usually strips it silently. Those three bytes are
hashed.
Line endings are part of the content. Real IODDs are commonly CRLF. Opening
a file in text mode on some platforms, or normalising newlines "for
consistency", changes every line in the document.
The trailing newline is part of the content. Some files end with one, some
don't. Neither should be added or removed.
The XML must not be reparsed. Round-tripping through a DOM and
reserialising will alter attribute quoting, self-closing tag style, entity
escaping, namespace declarations and whitespace — all semantically irrelevant,
all fatal to the checksum.
The practical rule is that the CRC is computed over an opaque byte array, not
over an XML document. Any tool that edits IODDs must therefore make surgical,
byte-preserving modifications rather than parse-and-rewrite ones.
One thing that is not a problem: non-ASCII text. Files containing German
umlauts, Cyrillic, Japanese or Chinese need no special handling, because
everything is treated as bytes. Encoding is a common suspect here, and a
misleading one.
Translation files are chained
IODD supports external language documents — separate files with an
<ExternalTextDocument> root, holding translations of a device's UI strings.
These get one extra step. After hashing their own bytes to end of file, the
decimal digits of the main IODD's CRC are appended to the stream as ASCII
characters, with no leading zeroes:
if (isExternalTextDocument(bytes)) {
crc.update(encoder.encode(String(mainIoddCrc)));
}
So if the main IODD's CRC is 794805970, the nine characters 7, 9, 4, …
are fed in as the final bytes.
This is a chaining construction, and it is deliberate. It binds a translation
to one specific revision of the device description it translates. Edit the main
IODD and every language file's stamp becomes invalid, which prevents a stale
translation from silently pairing with a newer device model.
The order of operations follows from this: stamp the main IODD first, then
the translations.
Only ExternalTextDocument behaves this way. IODevice,
IODDStandardDefinitions and IODDStandardUnitDefinitions hash their own bytes
and stop.
A note on a persistent myth
A frequently-cited claim holds that the shipped IODD-Checker uses a different
algorithm from the published specification, and that the documented procedure
cannot be made to work.
Testing against 40 officially stamped documents — spanning checker versions
V1.1.1, V1.1.5, V1.1.13 and V2025.1 — shows this is not the case. The
specification is accurate as written, and the algorithm has been stable across
roughly a decade of tool releases.
The confusion is easy to account for. Every one of the "binary mode" pitfalls
above produces a mismatch, and most of them happen invisibly, without any
explicit line of code choosing them. A stripped BOM or a normalised newline
looks like a broken specification rather than a helpful runtime.
Verification
The implementation in this repository is validated against:
| 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 |
11 of those are ExternalTextDocument files exercising the chained path.
Each file is checked three ways: the officially stamped value must be reproduced
exactly, re-stamping must be byte-identical to the original, and flipping a
single bit anywhere must be detected.
The vendor file is disproportionately valuable as a test case, because it
differs from the specification examples in four independent ways — it has a BOM,
uses CRLF, has no trailing newline, and was stamped by the oldest checker
version in the set. Matching it confirms all four rules at once.
Using it
IODDForge Checker is MIT
licensed with zero dependencies, and runs in Node, Bun, Deno and the browser.
ioddforge-crc verify ./my-device-IODD1.1.xml
ioddforge-crc write ./iodd-folder/
import { applyStampCrc } from 'ioddforge-checker';
const { bytes, crc } = applyStampCrc(fileBytes);
applyStampCrc replaces only the digits inside crc="…" and leaves every other
byte untouched, which keeps it safe to run against vendor files.
Why it matters
The stamp is the last gate between an authored IODD and a usable one. Without a
way to compute it, no open-source tool can produce a file that real IO-Link
tooling will accept — which has made the closed-source checker a hard dependency
for anyone building in this space.
It also imposes a useful discipline on editors. Because the checksum covers the
raw bytes, any tool that edits IODDs is obliged to preserve formatting exactly.
The mandatory checksum turns out to be a rather strict correctness test,
enforced on every save.
Top comments (0)