Skip to content

Commit 557487d

Browse files
authored
fix: XLSX export producing corrupted files in Excel (#464) (#468)
Signed-off-by: Ngô Quốc Đạt <datlechin@gmail.com>
1 parent 2630c3e commit 557487d

File tree

2 files changed

+14
-5
lines changed

2 files changed

+14
-5
lines changed

CHANGELOG.md

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -9,6 +9,7 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
99

1010
### Fixed
1111

12+
- XLSX export producing corrupted files that Excel cannot open (#464)
1213
- Deep link cold launch missing toolbar and duplicate windows (#465)
1314

1415
### Added

Plugins/XLSXExportPlugin/XLSXWriter.swift

Lines changed: 13 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -265,7 +265,9 @@ final class XLSXWriter {
265265
d.appendUTF8("<cellXfs count=\"2\">")
266266
d.appendUTF8("<xf numFmtId=\"0\" fontId=\"0\" fillId=\"0\" borderId=\"0\" xfId=\"0\"/>")
267267
d.appendUTF8("<xf numFmtId=\"0\" fontId=\"1\" fillId=\"0\" borderId=\"0\" xfId=\"0\" applyFont=\"1\"/>")
268-
d.appendUTF8("</cellXfs></styleSheet>")
268+
d.appendUTF8("</cellXfs>")
269+
d.appendUTF8("<cellStyles count=\"1\"><cellStyle name=\"Normal\" xfId=\"0\" builtinId=\"0\"/></cellStyles>")
270+
d.appendUTF8("</styleSheet>")
269271
return d
270272
}
271273

@@ -309,7 +311,9 @@ private extension Data {
309311
} ?? self.append(contentsOf: string.utf8)
310312
}
311313

312-
/// Append XML-escaped text directly to Data without creating intermediate Strings
314+
/// Append XML-escaped text directly to Data without creating intermediate Strings.
315+
/// Strips XML 1.0 illegal control characters (0x00–0x08, 0x0B, 0x0C, 0x0E–0x1F)
316+
/// that can appear in binary/hex database columns and would produce malformed XML.
313317
mutating func appendXMLEscaped(_ text: String) {
314318
for byte in text.utf8 {
315319
switch byte {
@@ -323,6 +327,10 @@ private extension Data {
323327
append(contentsOf: [0x26, 0x71, 0x75, 0x6F, 0x74, 0x3B]) // &quot;
324328
case 0x27: // '
325329
append(contentsOf: [0x26, 0x61, 0x70, 0x6F, 0x73, 0x3B]) // &apos;
330+
case 0x09, 0x0A, 0x0D: // Tab, LF, CR — allowed in XML 1.0
331+
append(byte)
332+
case 0x00...0x08, 0x0B, 0x0C, 0x0E...0x1F: // Illegal XML 1.0 control chars
333+
break // Strip silently
326334
default:
327335
append(byte)
328336
}
@@ -360,7 +368,7 @@ private enum ZipBuilder {
360368

361369
// Local file header
362370
output.append(contentsOf: [0x50, 0x4B, 0x03, 0x04]) // Signature
363-
output.appendUInt16(20) // Version needed
371+
output.appendUInt16(10) // Version needed (1.0 for store)
364372
output.appendUInt16(0) // Flags
365373
output.appendUInt16(0) // Compression: stored
366374
output.appendUInt16(0) // Mod time
@@ -375,8 +383,8 @@ private enum ZipBuilder {
375383

376384
// Central directory entry
377385
centralDirectory.append(contentsOf: [0x50, 0x4B, 0x01, 0x02])
378-
centralDirectory.appendUInt16(20)
379-
centralDirectory.appendUInt16(20)
386+
centralDirectory.appendUInt16(20) // Version made by (2.0)
387+
centralDirectory.appendUInt16(10) // Version needed (1.0 for store)
380388
centralDirectory.appendUInt16(0)
381389
centralDirectory.appendUInt16(0)
382390
centralDirectory.appendUInt16(0)

0 commit comments

Comments
 (0)