fix(#2097): escape trailing-NUL signatures so XML and JSON round trip - #2103
Conversation
|
icGetSigStr() selected its unambiguous "%08Xh" escape only when a zero byte was followed by a non-zero one. A signature whose zeros were all trailing therefore took the four-character path, where the zero simply terminated the C string the loop was filling: 'DUP\0' (0x44555000) was written as "DUP", and icGetSigVal() right-pads a three-character signature with 0x20, so it came back as 'DUP ' (0x44555020). Confirming @xsscx's RCA. Both machine-format writers serialize tag-table identifiers through this display helper -- IccProfileXml.cpp:273 and IccProfileJson.cpp:228 -- so an ICC -> XML -> ICC or ICC -> JSON -> ICC trip silently rewrote four bytes of a conformant profile. Measured against the tracked ISO 22028 fixture with a 0x44555000 private tag, exactly one byte changed (offset 172, 00 -> 20) in both lanes, and both the original and the round-tripped profile report "Profile is valid for version 5.00": 0x44555020 is unregistered, not malformed. Any zero byte now takes the hex form, which icGetSigVal() parses back exactly. That covers the six ranges the live tag registry opens on such a signature (44555000 and 64757000 for Dupont, 544B0000, 546B0000, 744B0000 and 746B0000 for Tektronix, checked 2026-08-11). icGetColorSigStr() carried the same condition and is likewise a serializer -- it renders <DataColourSpace>, <PCS> and <MCS> -- so it gets the same treatment. The seventh row of the issue's table, a signature that is zero in all four bytes, is deliberately not addressed here: icGetSigStr(0) returns the literal "NULL", a display convention the call sites work around by writing an empty value for zero, which is the #1356 / #1361 / #1843 family with its own three regressions. Changing it would alter iccDumpProfile output for every absent signature, and the XML reader rejects an empty TagSignature outright, so that row needs a design decision rather than a formatter change. The new regression asserts byte-exact round trips for all six signatures across both formats and the cross-format lane, plus one anti-overfit check: escaping every signature to hex would satisfy the other assertions while making every serialized profile unreadable, so a printable signature must still be written as its four characters.
09d4f89 to
61caed9
Compare
PR Status2026-08-11 10:26:35 UTC MonitoringPotentially a transient error... waiting on the job to complete and review logs.. CI Error |
PR Status2026-08-11 10:37:11 UTC
|
Part of #2097 — the trailing-NUL half. Confirming @xsscx's RCA in full.
The defect
icGetSigStr()(IccProfLib/IccUtil.cpp:1117) renders a signature as fourcharacters when it can and falls back to an unambiguous
"%08Xh"escape when itcannot. The fallback was selected only by a zero byte followed by a non-zero
one. A signature whose zeros were all trailing therefore took the
four-character path, where the zero simply terminated the C string the loop was
filling:
Both machine-format writers serialize tag-table identifiers through this display
helper —
IccProfileXml.cpp:273andIccProfileJson.cpp:228— so the value wasrewritten on every round trip in either format.
Measured
Tracked fixture
Testing/Encoding/ISO22028-Encoded-sRGB.xmlplus onesignatureTypetag carrying0x44555000:ICC -> XML -> ICC44 55 50 00->44 55 50 20ICC -> JSON -> ICC44 55 50 00->44 55 50 20ICC -> JSON -> ICC -> XML -> ICCExactly one byte moved, at offset 172. Both the correct and the corrupted
profile validate as "Profile is valid for version 5.00" —
0x44555020ismerely unregistered, not malformed — which is why the regression asserts bytes
rather than a validation report.
Checked against the live registry (
registry.color.org/tag-signatures,2026-08-11): six assigned ranges open on such a signature —
44555000and64757000(Dupont),544B0000,546B0000,744B0000,746B0000(Tektronix). All six now survive both formats.
The fix
Any zero byte selects the hex form, which
icGetSigVal()parses back exactly(its 9-character case). Every reader on both sides already routes through
icGetSigVal— XML viaicXmlStrToSig, JSON directly — so this is writer-onlyand symmetric.
icGetColorSigStr()carried the identical condition and is likewise aserializer (
<DataColourSpace>,<PCS>,<MCS>,<SpectralPCS>), so it getsthe same treatment.
Deliberately not addressed here
The seventh row of the issue's table,
00000000(Sun Microsystems' assignedrange start), still round-trips to
4E554C4C.icGetSigStr(0)returns theliteral text
"NULL", which is a display convention the call sites workaround by writing an empty value for zero — the #1356 / #1361 / #1843 family,
which has three regressions of its own and whose readers map empty text back to
zero. Changing it would alter
iccDumpProfileoutput for every absentsignature, and the XML reader rejects an empty
TagSignatureattributeoutright, so a fix for the all-zero tag-table identifier needs a design
decision rather than a formatter change. Flagged for triage, not silently
folded in.
Regression
iccdev.issue-2097-nul-signature-roundtrip— new script test, fixture derivedfrom tracked XML by
awk(no new binaries). Four sections: the reported examplein both formats; the five remaining registry signatures; the cross-format lane;
and an anti-overfit check — escaping every signature to hex would satisfy the
first three while making every serialized profile unreadable, so a printable
signature must still be written as its four characters. Verified red against a
reverted library (fails at the first assertion) and green on restore.
Pre-flight
-Wall -Wextra -Wpedantic -WerrorReleasestrict warnings ENABLED)detect_leaks=1ctest(serial and-j4)iccdev.spectral-tiff-preview, a local-only missing pythonimagecodecsshellcheck0.9.0 on the new scriptThe three sibling zero-signature regressions (#1356, #1361, #1843) all still
pass, confirming the
"NULL"convention is untouched.