ODS writer: drop duplicate XML declaration; harden sheet-name sanitiser.

LibreOffice rejected the generated `.ods` with `Format error at 2,39
in content.xml`. Root cause: `pugi::format_no_declaration` suppresses
only the *implicit* declaration auto-added at save time — the explicit
`node_declaration` I had appended to the document still got serialised,
on top of a manual `<?xml…?>` string prepend in the output. Two
declarations back-to-back, invalid XML.

Fix: let pugixml emit the explicit declaration node, drop the manual
prepend.

Also harden the sheet-name sanitiser in the export action: ODS / Excel
also forbid `< > &` in raw cell or table names, so the default
connection name `bp/J20 <-> payload1/P0` made content.xml entity-
escape `<` to `&lt;`, which a few viewers handle but Excel rejects.
Clip to 31 chars too (Excel's hard limit) so multi-name connections
don't blow up the open.

Verified by `soffice --headless --convert-to csv` round-tripping the
output without errors.

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
This commit is contained in:
2026-05-16 12:22:51 +02:00
parent 67de4dcaf3
commit aa59d1a041
2 changed files with 17 additions and 8 deletions

View File

@@ -73,11 +73,14 @@ std::string build_content_xml(const std::vector<OdsSheet> &sheets) {
}
std::ostringstream oss;
doc.save(oss, " ", pugi::format_no_declaration | pugi::format_raw);
// pugi's `format_no_declaration` drops the `<?xml…?>` line; we want
// it. Re-emit explicitly to control encoding.
return std::string(R"(<?xml version="1.0" encoding="UTF-8"?>)" "\n")
+ oss.str();
// We added the declaration node explicitly above, so pugi will emit
// it. `format_no_declaration` only suppresses *implicit* injection;
// setting it here would leave the explicit node in place and silently
// duplicate would-be auto-emission elsewhere. Keep format_raw to
// avoid pretty-print overhead — the file is read by software, not
// humans.
doc.save(oss, "", pugi::format_raw);
return oss.str();
}
// Minimum META-INF/manifest.xml that satisfies the spec.

View File

@@ -98,12 +98,18 @@ void Tui::RegisterExportCommands() {
int total = 0;
for (auto &ckv : *sys->connections()) {
Connection *c = ckv.second;
// Sheet names: Excel rejects /\?*:[] characters
// (LibreOffice tolerates them but stays portable).
// Sheet names: Excel rejects /\?*:[] characters,
// ODS forbids < > & in raw cell/table names
// (they'd need entity-escaping but a few viewers
// choke on them). Sanitise to underscores.
std::string sname = c->name;
for (char &ch : sname)
if (ch == '/' || ch == '\\' || ch == '?' || ch == '*'
|| ch == ':' || ch == '[' || ch == ']') ch = '_';
|| ch == ':' || ch == '[' || ch == ']'
|| ch == '<' || ch == '>' || ch == '&') ch = '_';
// Sheet names also can't exceed 31 chars in
// Excel; clip aggressively to stay portable.
if (sname.size() > 31) sname = sname.substr(0, 31);
OdsSheet *s = w.add_sheet(sname);
const char *hdr[] = {
"transform",