diff --git a/CHANGELOG.md b/CHANGELOG.md
index 8bfafa51..a197dac0 100644
--- a/CHANGELOG.md
+++ b/CHANGELOG.md
@@ -9,6 +9,7 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
### Fixed
+- XLSX export producing corrupted files that Excel cannot open (#464)
- Deep link cold launch missing toolbar and duplicate windows (#465)
### Added
diff --git a/Plugins/XLSXExportPlugin/XLSXWriter.swift b/Plugins/XLSXExportPlugin/XLSXWriter.swift
index 0e45be1f..89370cdc 100644
--- a/Plugins/XLSXExportPlugin/XLSXWriter.swift
+++ b/Plugins/XLSXExportPlugin/XLSXWriter.swift
@@ -265,7 +265,9 @@ final class XLSXWriter {
d.appendUTF8("")
d.appendUTF8("")
d.appendUTF8("")
- d.appendUTF8("")
+ d.appendUTF8("")
+ d.appendUTF8("")
+ d.appendUTF8("")
return d
}
@@ -309,7 +311,9 @@ private extension Data {
} ?? self.append(contentsOf: string.utf8)
}
- /// Append XML-escaped text directly to Data without creating intermediate Strings
+ /// Append XML-escaped text directly to Data without creating intermediate Strings.
+ /// Strips XML 1.0 illegal control characters (0x00ā0x08, 0x0B, 0x0C, 0x0Eā0x1F)
+ /// that can appear in binary/hex database columns and would produce malformed XML.
mutating func appendXMLEscaped(_ text: String) {
for byte in text.utf8 {
switch byte {
@@ -323,6 +327,10 @@ private extension Data {
append(contentsOf: [0x26, 0x71, 0x75, 0x6F, 0x74, 0x3B]) // "
case 0x27: // '
append(contentsOf: [0x26, 0x61, 0x70, 0x6F, 0x73, 0x3B]) // '
+ case 0x09, 0x0A, 0x0D: // Tab, LF, CR ā allowed in XML 1.0
+ append(byte)
+ case 0x00...0x08, 0x0B, 0x0C, 0x0E...0x1F: // Illegal XML 1.0 control chars
+ break // Strip silently
default:
append(byte)
}
@@ -360,7 +368,7 @@ private enum ZipBuilder {
// Local file header
output.append(contentsOf: [0x50, 0x4B, 0x03, 0x04]) // Signature
- output.appendUInt16(20) // Version needed
+ output.appendUInt16(10) // Version needed (1.0 for store)
output.appendUInt16(0) // Flags
output.appendUInt16(0) // Compression: stored
output.appendUInt16(0) // Mod time
@@ -375,8 +383,8 @@ private enum ZipBuilder {
// Central directory entry
centralDirectory.append(contentsOf: [0x50, 0x4B, 0x01, 0x02])
- centralDirectory.appendUInt16(20)
- centralDirectory.appendUInt16(20)
+ centralDirectory.appendUInt16(20) // Version made by (2.0)
+ centralDirectory.appendUInt16(10) // Version needed (1.0 for store)
centralDirectory.appendUInt16(0)
centralDirectory.appendUInt16(0)
centralDirectory.appendUInt16(0)