@@ -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 ] ) // "
324328 case 0x27 : // '
325329 append ( contentsOf: [ 0x26 , 0x61 , 0x70 , 0x6F , 0x73 , 0x3B ] ) // '
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