@@ -84,9 +84,7 @@ private async Task SavePathAsync(CancellationToken cancellationToken)
8484 using ( var source = new FileStream ( _path ! , FileMode . Open , FileAccess . Read , FileShare . Read ) )
8585 using ( var temporaryStream = new FileStream ( temporaryPath , FileMode . CreateNew , FileAccess . ReadWrite , FileShare . None ) )
8686 {
87- await source . CopyToAsync ( temporaryStream , 81920 , cancellationToken ) . ConfigureAwait ( false ) ;
88- temporaryStream . Position = 0 ;
89- await ApplyUpdatesAsync ( temporaryStream , cancellationToken ) . ConfigureAwait ( false ) ;
87+ await ApplyUpdatesAsync ( source , temporaryStream , cancellationToken ) . ConfigureAwait ( false ) ;
9088 await temporaryStream . FlushAsync ( cancellationToken ) . ConfigureAwait ( false ) ;
9189 }
9290
@@ -109,9 +107,7 @@ private async Task SaveStreamAsync(Stream stream, CancellationToken cancellation
109107 try
110108 {
111109 using var temporaryStream = new FileStream ( temporaryPath , FileMode . Create , FileAccess . ReadWrite , FileShare . None ) ;
112- await stream . CopyToAsync ( temporaryStream , 81920 , cancellationToken ) . ConfigureAwait ( false ) ;
113- temporaryStream . Position = 0 ;
114- await ApplyUpdatesAsync ( temporaryStream , cancellationToken ) . ConfigureAwait ( false ) ;
110+ await ApplyUpdatesAsync ( stream , temporaryStream , cancellationToken ) . ConfigureAwait ( false ) ;
115111
116112 temporaryStream . Position = 0 ;
117113 stream . Position = 0 ;
@@ -125,49 +121,49 @@ private async Task SaveStreamAsync(Stream stream, CancellationToken cancellation
125121 }
126122 }
127123
128- private async Task ApplyUpdatesAsync ( Stream stream , CancellationToken cancellationToken )
124+ private async Task ApplyUpdatesAsync ( Stream inputStream , Stream outputStream , CancellationToken cancellationToken )
129125 {
130- stream . Seek ( 0 , SeekOrigin . Begin ) ;
131- using var archive = new ZipArchive ( stream , ZipArchiveMode . Update , leaveOpen : true ) ;
126+ inputStream . Seek ( 0 , SeekOrigin . Begin ) ;
127+ using var inputArchive = new ZipArchive ( inputStream , ZipArchiveMode . Read , leaveOpen : true ) ;
132128
133- var contentTypes = await LoadDocumentAsync ( GetRequiredEntry ( archive , ExcelFileNames . ContentTypes ) , cancellationToken ) . ConfigureAwait ( false ) ;
129+ var contentTypes = await LoadDocumentAsync ( GetRequiredEntry ( inputArchive , ExcelFileNames . ContentTypes ) , cancellationToken ) . ConfigureAwait ( false ) ;
134130 if ( contentTypes . Descendants ( ) . Attributes ( "ContentType" )
135131 . Any ( attribute => attribute . Value . IndexOf ( "macroEnabled" , StringComparison . OrdinalIgnoreCase ) >= 0 ) )
136132 throw new NotSupportedException ( "MiniExcel's OpenXml editor does not support the .xlsm format." ) ;
137133
138- var workbook = await LoadDocumentAsync ( GetRequiredEntry ( archive , ExcelFileNames . Workbook ) , cancellationToken ) . ConfigureAwait ( false ) ;
139- var workbookRelationships = await LoadDocumentAsync ( GetRequiredEntry ( archive , ExcelFileNames . WorkbookRels ) , cancellationToken ) . ConfigureAwait ( false ) ;
134+ var workbook = await LoadDocumentAsync ( GetRequiredEntry ( inputArchive , ExcelFileNames . Workbook ) , cancellationToken ) . ConfigureAwait ( false ) ;
135+ var workbookRelationships = await LoadDocumentAsync ( GetRequiredEntry ( inputArchive , ExcelFileNames . WorkbookRels ) , cancellationToken ) . ConfigureAwait ( false ) ;
140136 var sheets = GetSheets ( workbook , workbookRelationships ) ;
141137 var pendingUpdates = ResolveUpdates ( sheets ) ;
142138
143- var stylesEntry = GetRequiredEntry ( archive , ExcelFileNames . Styles ) ;
139+ var stylesEntry = GetRequiredEntry ( inputArchive , ExcelFileNames . Styles ) ;
144140 var styles = await LoadDocumentAsync ( stylesEntry , cancellationToken ) . ConfigureAwait ( false ) ;
145141 var styleContext = new StyleUpdateContext ( styles ) ;
146- var worksheetDocuments = new Dictionary < string , XDocument > ( StringComparer . OrdinalIgnoreCase ) ;
142+ var updatesBySheet = pendingUpdates
143+ . GroupBy ( update => update . Sheet . Path , StringComparer . OrdinalIgnoreCase )
144+ . ToList ( ) ;
145+ var worksheetPaths = new HashSet < string > (
146+ updatesBySheet . Select ( group => group . Key ) ,
147+ StringComparer . OrdinalIgnoreCase ) ;
147148
148- foreach ( var update in pendingUpdates )
149+ using var outputArchive = new ZipArchive ( outputStream , ZipArchiveMode . Create , leaveOpen : true ) ;
150+ foreach ( var inputEntry in inputArchive . Entries )
149151 {
150- cancellationToken . ThrowIfCancellationRequested ( ) ;
151-
152- if ( ! worksheetDocuments . TryGetValue ( update . Sheet . Path , out var worksheet ) )
153- {
154- worksheet = await LoadDocumentAsync ( GetRequiredEntry ( archive , update . Sheet . Path ) , cancellationToken ) . ConfigureAwait ( false ) ;
155- worksheetDocuments . Add ( update . Sheet . Path , worksheet ) ;
156- }
152+ if ( inputEntry . FullName . Equals ( ExcelFileNames . Styles , StringComparison . OrdinalIgnoreCase )
153+ || worksheetPaths . Contains ( inputEntry . FullName ) )
154+ continue ;
157155
158- var worksheetNamespace = worksheet . Root ? . Name . Namespace
159- ?? throw new InvalidDataException ( $ "Worksheet '{ update . Sheet . Name } ' has no root element.") ;
160- var cell = worksheet . Descendants ( worksheetNamespace + "c" )
161- . FirstOrDefault ( element => string . Equals ( element . Attribute ( "r" ) ? . Value , update . CellReference , StringComparison . OrdinalIgnoreCase ) )
162- ?? throw new InvalidDataException ( $ "Cell '{ update . CellReference } ' does not exist in worksheet '{ update . Sheet . Name } '.") ;
156+ await CopyEntryAsync ( inputEntry , outputArchive , cancellationToken ) . ConfigureAwait ( false ) ;
157+ }
163158
164- var originalStyleIndex = ParseStyleIndex ( cell . Attribute ( "s" ) ? . Value , update . CellReference ) ;
165- cell . SetAttributeValue ( "s" , styleContext . GetStyleIndex ( originalStyleIndex , update . FontColor ) ) ;
159+ foreach ( var sheetUpdates in updatesBySheet )
160+ {
161+ var inputEntry = GetRequiredEntry ( inputArchive , sheetUpdates . Key ) ;
162+ var outputEntry = CreateEntry ( outputArchive , inputEntry ) ;
163+ await RewriteWorksheetAsync ( inputEntry , outputEntry , sheetUpdates , styleContext , cancellationToken ) . ConfigureAwait ( false ) ;
166164 }
167165
168- await ReplaceEntryAsync ( archive , ExcelFileNames . Styles , styles , cancellationToken ) . ConfigureAwait ( false ) ;
169- foreach ( var worksheet in worksheetDocuments )
170- await ReplaceEntryAsync ( archive , worksheet . Key , worksheet . Value , cancellationToken ) . ConfigureAwait ( false ) ;
166+ await WriteDocumentEntryAsync ( outputArchive , stylesEntry , styles , cancellationToken ) . ConfigureAwait ( false ) ;
171167 }
172168
173169 private static void ReplaceFile ( string sourcePath , string destinationPath )
@@ -253,12 +249,157 @@ private static async Task<XDocument> LoadDocumentAsync(ZipArchiveEntry entry, Ca
253249 return await XDocument . LoadAsync ( stream , LoadOptions . PreserveWhitespace , cancellationToken ) . ConfigureAwait ( false ) ;
254250 }
255251
256- private static async Task ReplaceEntryAsync ( ZipArchive archive , string path , XDocument document , CancellationToken cancellationToken )
252+ private static async Task CopyEntryAsync ( ZipArchiveEntry inputEntry , ZipArchive outputArchive , CancellationToken cancellationToken )
257253 {
258- archive . GetEntry ( path ) ? . Delete ( ) ;
259- var entry = archive . CreateEntry ( path , CompressionLevel . Optimal ) ;
260- using var stream = await entry . OpenAsync ( cancellationToken ) . ConfigureAwait ( false ) ;
261- await document . SaveAsync ( stream , SaveOptions . DisableFormatting , cancellationToken ) . ConfigureAwait ( false ) ;
254+ var outputEntry = CreateEntry ( outputArchive , inputEntry ) ;
255+ if ( inputEntry . FullName . EndsWith ( "/" , StringComparison . Ordinal ) )
256+ return ;
257+
258+ using var inputStream = await inputEntry . OpenAsync ( cancellationToken ) . ConfigureAwait ( false ) ;
259+ using var outputStream = await outputEntry . OpenAsync ( cancellationToken ) . ConfigureAwait ( false ) ;
260+ await inputStream . CopyToAsync ( outputStream , 81920 , cancellationToken ) . ConfigureAwait ( false ) ;
261+ }
262+
263+ private static async Task RewriteWorksheetAsync ( ZipArchiveEntry inputEntry , ZipArchiveEntry outputEntry ,
264+ IEnumerable < ResolvedStyleUpdate > updates , StyleUpdateContext styleContext , CancellationToken cancellationToken )
265+ {
266+ var pendingUpdates = updates . ToDictionary ( update => update . CellReference , StringComparer . OrdinalIgnoreCase ) ;
267+ using var inputStream = await inputEntry . OpenAsync ( cancellationToken ) . ConfigureAwait ( false ) ;
268+ using var outputStream = await outputEntry . OpenAsync ( cancellationToken ) . ConfigureAwait ( false ) ;
269+ using var reader = XmlReader . Create ( inputStream , new XmlReaderSettings
270+ {
271+ Async = true ,
272+ CloseInput = false ,
273+ DtdProcessing = DtdProcessing . Prohibit
274+ } ) ;
275+ using var writer = XmlWriter . Create ( outputStream , new XmlWriterSettings
276+ {
277+ Async = true ,
278+ CloseOutput = false ,
279+ Encoding = new UTF8Encoding ( false ) ,
280+ Indent = false
281+ } ) ;
282+
283+ while ( await reader . ReadAsync ( ) . ConfigureAwait ( false ) )
284+ {
285+ cancellationToken . ThrowIfCancellationRequested ( ) ;
286+
287+ if ( reader . NodeType == XmlNodeType . Element && reader . LocalName == "c"
288+ && reader . GetAttribute ( "r" ) is { } cellReference
289+ && pendingUpdates . TryGetValue ( cellReference , out var update ) )
290+ {
291+ var originalStyleIndex = ParseStyleIndex ( reader . GetAttribute ( "s" ) , cellReference ) ;
292+ var styleIndex = styleContext . GetStyleIndex ( originalStyleIndex , update . FontColor ) ;
293+ await WriteCellStartElementAsync ( reader , writer , styleIndex ) . ConfigureAwait ( false ) ;
294+ pendingUpdates . Remove ( cellReference ) ;
295+ continue ;
296+ }
297+
298+ await WriteCurrentNodeAsync ( reader , writer ) . ConfigureAwait ( false ) ;
299+ }
300+
301+ if ( pendingUpdates . Count > 0 )
302+ {
303+ var missingCell = pendingUpdates . Values . OrderBy ( update => update . Row ) . ThenBy ( update => update . Column ) . First ( ) ;
304+ throw new InvalidDataException ( $ "Cell '{ missingCell . CellReference } ' does not exist in worksheet '{ missingCell . Sheet . Name } '.") ;
305+ }
306+
307+ await writer . FlushAsync ( ) . ConfigureAwait ( false ) ;
308+ }
309+
310+ private static async Task WriteCellStartElementAsync ( XmlReader reader , XmlWriter writer , int styleIndex )
311+ {
312+ await writer . WriteStartElementAsync ( reader . Prefix , reader . LocalName , reader . NamespaceURI ) . ConfigureAwait ( false ) ;
313+ var wroteStyle = false ;
314+ if ( reader . MoveToFirstAttribute ( ) )
315+ {
316+ do
317+ {
318+ if ( reader . LocalName == "s" && reader . NamespaceURI . Length == 0 )
319+ {
320+ await writer . WriteAttributeStringAsync ( null , "s" , null , styleIndex . ToString ( CultureInfo . InvariantCulture ) ) . ConfigureAwait ( false ) ;
321+ wroteStyle = true ;
322+ }
323+ else
324+ {
325+ await writer . WriteAttributeStringAsync ( reader . Prefix , reader . LocalName , reader . NamespaceURI , reader . Value ) . ConfigureAwait ( false ) ;
326+ }
327+ }
328+ while ( reader . MoveToNextAttribute ( ) ) ;
329+
330+ reader . MoveToElement ( ) ;
331+ }
332+
333+ if ( ! wroteStyle )
334+ await writer . WriteAttributeStringAsync ( null , "s" , null , styleIndex . ToString ( CultureInfo . InvariantCulture ) ) . ConfigureAwait ( false ) ;
335+
336+ if ( reader . IsEmptyElement )
337+ await writer . WriteEndElementAsync ( ) . ConfigureAwait ( false ) ;
338+ }
339+
340+ private static async Task WriteCurrentNodeAsync ( XmlReader reader , XmlWriter writer )
341+ {
342+ switch ( reader . NodeType )
343+ {
344+ case XmlNodeType . Element :
345+ await writer . WriteStartElementAsync ( reader . Prefix , reader . LocalName , reader . NamespaceURI ) . ConfigureAwait ( false ) ;
346+ if ( reader . MoveToFirstAttribute ( ) )
347+ {
348+ do
349+ {
350+ await writer . WriteAttributeStringAsync ( reader . Prefix , reader . LocalName , reader . NamespaceURI , reader . Value ) . ConfigureAwait ( false ) ;
351+ }
352+ while ( reader . MoveToNextAttribute ( ) ) ;
353+
354+ reader . MoveToElement ( ) ;
355+ }
356+ if ( reader . IsEmptyElement )
357+ await writer . WriteEndElementAsync ( ) . ConfigureAwait ( false ) ;
358+ break ;
359+ case XmlNodeType . EndElement :
360+ await writer . WriteFullEndElementAsync ( ) . ConfigureAwait ( false ) ;
361+ break ;
362+ case XmlNodeType . Text :
363+ await writer . WriteStringAsync ( reader . Value ) . ConfigureAwait ( false ) ;
364+ break ;
365+ case XmlNodeType . CDATA :
366+ await writer . WriteCDataAsync ( reader . Value ) . ConfigureAwait ( false ) ;
367+ break ;
368+ case XmlNodeType . Whitespace :
369+ case XmlNodeType . SignificantWhitespace :
370+ await writer . WriteWhitespaceAsync ( reader . Value ) . ConfigureAwait ( false ) ;
371+ break ;
372+ case XmlNodeType . Comment :
373+ await writer . WriteCommentAsync ( reader . Value ) . ConfigureAwait ( false ) ;
374+ break ;
375+ case XmlNodeType . ProcessingInstruction :
376+ await writer . WriteProcessingInstructionAsync ( reader . Name , reader . Value ) . ConfigureAwait ( false ) ;
377+ break ;
378+ case XmlNodeType . XmlDeclaration :
379+ await writer . WriteStartDocumentAsync ( ) . ConfigureAwait ( false ) ;
380+ break ;
381+ case XmlNodeType . DocumentType :
382+ await writer . WriteDocTypeAsync ( reader . Name , reader . GetAttribute ( "PUBLIC" ) , reader . GetAttribute ( "SYSTEM" ) , reader . Value ) . ConfigureAwait ( false ) ;
383+ break ;
384+ case XmlNodeType . EntityReference :
385+ await writer . WriteEntityRefAsync ( reader . Name ) . ConfigureAwait ( false ) ;
386+ break ;
387+ }
388+ }
389+
390+ private static async Task WriteDocumentEntryAsync ( ZipArchive outputArchive , ZipArchiveEntry inputEntry ,
391+ XDocument document , CancellationToken cancellationToken )
392+ {
393+ var outputEntry = CreateEntry ( outputArchive , inputEntry ) ;
394+ using var outputStream = await outputEntry . OpenAsync ( cancellationToken ) . ConfigureAwait ( false ) ;
395+ await document . SaveAsync ( outputStream , SaveOptions . DisableFormatting , cancellationToken ) . ConfigureAwait ( false ) ;
396+ }
397+
398+ private static ZipArchiveEntry CreateEntry ( ZipArchive archive , ZipArchiveEntry sourceEntry )
399+ {
400+ var entry = archive . CreateEntry ( sourceEntry . FullName , CompressionLevel . Optimal ) ;
401+ entry . LastWriteTime = sourceEntry . LastWriteTime ;
402+ return entry ;
262403 }
263404
264405 private sealed class StyleUpdateContext
0 commit comments