Files
apk b6dc11fce0 fix(xml): replace characters XML cannot carry, escape paths once
Closes #375.

A snippet containing a NUL, an ANSI escape, a form feed or any other
control byte made the xml report unparseable: XML 1.0 has no way to
represent those characters, not even inside CDATA, so xmllint stopped at
"PCDATA invalid Char value 27" and every parser refused the file. The
reporter now replaces every character outside XML's Char production
(tab, LF, CR and U+0020 and up, minus U+FFFE/U+FFFF) with U+FFFD in
snippets and in path attributes. The existing "]]>" split is kept.

Path attributes were also escaped twice: quick-xml escapes attribute
values on its own, so the reporter's escape_xml() turned "r&d" into
"r&d". The manual escape is gone and the sanitized path goes in
raw.

Tests parse the generated report to the end with quick-xml and check:
control characters become U+FFFD and none survive, a "]]>" round-trips
through the split sections, a path with & < " > is escaped exactly once
and reads back unchanged, and legal text is left borrowed.

Fixtures: fixtures/xml-report-demo with a clone whose body holds real
escape bytes, a form feed and a "]]>" literal; the README shows the
xmllint and Python checks. Verified the published 5.2.0 fails xmllint
on it and the fixed binary passes.

Claude-Session: https://claude.ai/code/session_01WztdFKZAwW9b51iJe9zhtV
2026-09-13 07:44:22 +02:00
..

xml-report demo

Shows that the xml reporter produces well-formed XML even when a clone contains bytes XML cannot represent (issue #375). All commands run from the repository root at default thresholds; xmllint ships with macOS and libxml2 on Linux.

Directory Contents Default scan
escapes/ Two JavaScript files sharing a function whose body holds real ANSI escape bytes (0x1B), a form feed (0x0C) and a "]]>" literal Found 1 clones.

escapes/: a clone the XML report has to carry

banner.js and banner-copy.js differ only in the function name, so the clone spans the whole body, escapes included.

jscpd fixtures/xml-report-demo --reporters xml,console --output report
# Clone found (javascript)
#  - escapes/banner-copy.js [5:33 - 18:2] (14 lines, 115 tokens)
#    escapes/banner.js [5:28 - 18:2]
# Found 1 clones.
# XML report saved to report/jscpd-report.xml

xmllint --noout report/jscpd-report.xml
# (no output: the document is well-formed)

Inside the report the escape and form-feed bytes are replaced by U+FFFD, the replacement character, because XML 1.0 has no way to encode them, not even in CDATA. Every ]]> is split across two CDATA sections, so a parser reads the snippet text back unchanged:

grep -c '<!\[CDATA\[' report/jscpd-report.xml
# 6

python3 -c "import xml.dom.minidom as m; m.parse('report/jscpd-report.xml'); print('ok')"
# ok

Before the fix (jscpd 5.2.0 and earlier) the same command wrote the bytes verbatim and xmllint reported PCDATA invalid Char value 27 on the first escape and refused the file.