NanoXLSX.Writer 3.2.1
Loading...
Searching...
No Matches
XlsxWriter.cs
1/*
2 * NanoXLSX is a small .NET library to generate and read XLSX (Microsoft Excel 2007 or newer) files in an easy and native way
3 * Copyright Raphael Stoeckli © 2026
4 * This library is licensed under the MIT License.
5 * You find a copy of the license in project folder or on: http://opensource.org/licenses/MIT
6 */
7
8using NanoXLSX.Exceptions;
9using NanoXLSX.Interfaces;
10using NanoXLSX.Interfaces.Writer;
12using NanoXLSX.Registry;
13using NanoXLSX.Styles;
14using NanoXLSX.Utils;
15using System;
16using System.Collections.Generic;
17using System.IO;
18using System.IO.Packaging;
19using System.Linq;
20using System.Text;
21using System.Threading.Tasks;
22using System.Xml;
23using IOException = NanoXLSX.Exceptions.IOException;
24using PackagePartType = NanoXLSX.Internal.Structures.PackagePartDefinition.PackagePartType;
25using XmlElement = NanoXLSX.Utils.Xml.XmlElement;
26
28{
33 internal class XlsxWriter : IBaseWriter
34 {
35
36 #region staticFields
37 private static readonly DocumentPath WORKBOOK = new DocumentPath("workbook.xml", "xl/");
38 private static readonly DocumentPath STYLES = new DocumentPath("styles.xml", "xl/");
39 private static readonly DocumentPath APP_PROPERTIES = new DocumentPath("app.xml", "docProps/");
40 private static readonly DocumentPath CORE_PROPERTIES = new DocumentPath("core.xml", "docProps/");
41 private static readonly DocumentPath SHARED_STRINGS = new DocumentPath("sharedStrings.xml", "xl/");
42 private static readonly DocumentPath THEME = new DocumentPath("theme1.xml", "xl/theme/");
43 #endregion
44
45 #region privateFields
46 private int rootPackageIndex = 1;
47 private int xlPackageIndex = 1;
48 private Package package = null;
49
50 private readonly List<PackagePartDefinition> packagePartDefinitions = new List<PackagePartDefinition>();
51 private readonly Dictionary<string, Dictionary<string, PackagePart>> packageParts = new Dictionary<string, Dictionary<string, PackagePart>>();
52 private readonly Dictionary<int, DocumentPath> worksheetPaths = new Dictionary<int, DocumentPath>();
53 private readonly HashSet<string> preparedWriterFeatures = new HashSet<string>();
54 private readonly Dictionary<string, PackagePart> queuedPackageParts = new Dictionary<string, PackagePart>(StringComparer.Ordinal);
55
56 #endregion
57
58 #region properties
62 public Workbook Workbook { get; }
63
67 public IWriterProcessingData WriterProcessingData { get; set; }
68
72 public ISharedStringWriter SharedStringWriter { get; set; }
73
74 #endregion
75
76 #region constructors
81 public XlsxWriter(Workbook workbook)
82 {
83 this.Workbook = workbook;
84 }
85 #endregion
86
87 #region documentCreation_methods
88
97 public void Save()
98 {
99 try
100 {
101 FileStream fs = new FileStream(Workbook.Filename, FileMode.Create);
102 SaveAsStream(fs);
103
104 }
105 catch (Exception e)
106 {
107 throw new IOException("An error occurred while saving. See inner exception for details: " + e.Message, e);
108 }
109 }
110
116 public async Task SaveAsync()
117 {
118 await Task.Run(() => { Save(); });
119 }
120
128 public async Task SaveAsStreamAsync(Stream stream, bool leaveOpen = false)
129 {
130 await Task.Run(() => { SaveAsStream(stream, leaveOpen); });
131 }
132
139 public void SaveAsStream(Stream stream, bool leaveOpen = false)
140 {
141 preparedWriterFeatures.Clear();
142 WriterProcessingData = new WriterProcessingData(Workbook, StyleRepository.Instance);
143 try
144 {
145 // preparing processor(s)
146 IPluginWriteProcessor preparingProcessor = PlugInLoader.GetPlugIn<IPluginWriteProcessor>(PlugInUUID.PreparingProcessor, new PreparingProcessor());
147 preparingProcessor.Init(this, WriterPlugInHandler.HandleInlineQueueProcessorPlugins);
148 preparingProcessor.Execute();
149 // Compatibility check
150 CompatibilityProcessor compatibilityProcessor = new CompatibilityProcessor(); // This core processor cannot be overwritten
151 compatibilityProcessor.Init(this, WriterPlugInHandler.HandleInlineQueueProcessorPlugins);
152 compatibilityProcessor.Execute();
153 // Workbook can now be written
154 RegisterCommonPackageParts();
155 HandlePackageRegistryQueuePlugIns();
156 HandleQueuePlugIns(PlugInUUID.WriterPrependingQueue);
157
158 using (Package xlsxPackage = Package.Open(stream, FileMode.Create))
159 {
160 this.package = xlsxPackage;
161 PreparePackage();
162 PackagePart part;
163
164 // Workbook
165 IPluginWriter workbookWriter = PlugInLoader.GetPlugIn<IPluginWriter>(PlugInUUID.WorkbookWriter, new WorkbookWriter());
166 workbookWriter.Init(this);
167 workbookWriter.Execute();
168 part = packageParts[WORKBOOK.Path][WORKBOOK.Filename];
169 AppendXmlToPackagePart(workbookWriter.XmlElement, part);
170
171 // Style
172 IPluginWriter styleWriter = PlugInLoader.GetPlugIn<IPluginWriter>(PlugInUUID.StyleWriter, new StyleWriter());
173 styleWriter.Init(this);
174 styleWriter.Execute();
175 part = packageParts[STYLES.Path][STYLES.Filename];
176 AppendXmlToPackagePart(styleWriter.XmlElement, part);
177
178 // Shared strings - preparation
179 SharedStringWriter = PlugInLoader.GetPlugIn<ISharedStringWriter>(PlugInUUID.SharedStringsWriter, new SharedStringWriter());
180 SharedStringWriter.Init(this);
181 // Worksheets
182 IWorksheetWriter worksheetWriter = PlugInLoader.GetPlugIn<IWorksheetWriter>(PlugInUUID.WorksheetWriter, new WorksheetWriter());
183 worksheetWriter.Init(this);
184 if (Workbook.Worksheets.Count > 0)
185 {
186 for (int i = 0; i < Workbook.Worksheets.Count; i++)
187 {
188 Worksheet item = Workbook.Worksheets[i];
189 part = packageParts[worksheetPaths[i].Path][worksheetPaths[i].Filename];
190 worksheetWriter.CurrentWorksheet = item;
191 worksheetWriter.Execute();
192 AppendXmlToPackagePart(worksheetWriter.XmlElement, part);
193 worksheetWriter.ReleaseXmlElement();
194 GC.Collect(1, GCCollectionMode.Optimized); //
195 }
196 }
197 else
198 {
199 part = packageParts[worksheetPaths[0].Path][worksheetPaths[0].Filename];
200 worksheetWriter.CurrentWorksheet = new Worksheet("sheet1");
201 worksheetWriter.Execute();
202 AppendXmlToPackagePart(worksheetWriter.XmlElement, part);
203 worksheetWriter.ReleaseXmlElement();
204 }
205
206 // Shared strings - write after collection of strings
207 part = packageParts[SHARED_STRINGS.Path][SHARED_STRINGS.Filename];
208 SharedStringWriter.Execute();
209 AppendXmlToPackagePart(SharedStringWriter.XmlElement, part);
210
211 // Metadata
212 if (this.Workbook.WorkbookMetadata != null)
213 {
214 IPluginWriter metadataAppWriter = PlugInLoader.GetPlugIn<IPluginWriter>(PlugInUUID.MetadataAppWriter, new MetadataAppWriter());
215 metadataAppWriter.Init(this);
216 metadataAppWriter.Execute();
217 part = packageParts[APP_PROPERTIES.Path][APP_PROPERTIES.Filename];
218 AppendXmlToPackagePart(metadataAppWriter.XmlElement, part);
219 IPluginWriter metadataCoreWriter = PlugInLoader.GetPlugIn<IPluginWriter>(PlugInUUID.MetadataCoreWriter, new MetadataCoreWriter());
220 metadataCoreWriter.Init(this);
221 metadataCoreWriter.Execute();
222 part = packageParts[CORE_PROPERTIES.Path][CORE_PROPERTIES.Filename];
223 AppendXmlToPackagePart(metadataCoreWriter.XmlElement, part);
224 }
225
226 // Theme
227 if (Workbook.WorkbookTheme != null)
228 {
229 IPluginWriter themeWriter = PlugInLoader.GetPlugIn<IPluginWriter>(PlugInUUID.ThemeWriter, new ThemeWriter());
230 themeWriter.Init(this);
231 themeWriter.Execute();
232 part = packageParts[THEME.Path][THEME.Filename];
233 AppendXmlToPackagePart(themeWriter.XmlElement, part);
234 }
235
236 HandleQueuePlugIns(PlugInUUID.WriterAppendingQueue);
237
238 this.package.Flush();
239 this.package.Close();
240 if (!leaveOpen)
241 {
242 stream.Close();
243 }
244
245 }
246 Workbook.AuxiliaryData.ClearTemporaryData();
247 }
248 catch (Exception e)
249 {
250 throw new IOException("An error occurred while saving. See inner exception for details: " + e.Message, e);
251 }
252 }
253
257 private void RegisterCommonPackageParts()
258 {
259 // Workbook should always be the lowest index
260 RegisterPackagePart(PackagePartType.Root, PackagePartDefinition.WORKBOOK_PACKAGE_PART_INDEX, WORKBOOK, @"application/vnd.openxmlformats-officedocument.spreadsheetml.sheet.main+xml", @"http://schemas.openxmlformats.org/officeDocument/2006/relationships/officeDocument");
261 if (this.Workbook.WorkbookMetadata != null)
262 {
263 int index = PackagePartDefinition.METADATA_PACKAGE_PART_START_INDEX;
264 RegisterPackagePart(PackagePartType.Root, index, CORE_PROPERTIES, @"application/vnd.openxmlformats-package.core-properties+xml", @"http://schemas.openxmlformats.org/package/2006/relationships/metadata/core-properties");
265 RegisterPackagePart(PackagePartType.Root, index + 1000, APP_PROPERTIES, @"application/vnd.openxmlformats-officedocument.extended-properties+xml", @"http://schemas.openxmlformats.org/officeDocument/2006/relationships/extended-properties");
266 }
267 int worksheetOrderNumber = PackagePartDefinition.WORKSHEET_PACKAGE_PART_START_INDEX;
268 if (this.Workbook.Worksheets.Count == 0)
269 {
270 RegisterPackagePart(PackagePartType.Worksheet, worksheetOrderNumber, "sheet1.xml", "xl/worksheets", @"application/vnd.openxmlformats-officedocument.spreadsheetml.worksheet+xml", @"http://schemas.openxmlformats.org/officeDocument/2006/relationships/worksheet");
271 }
272 else
273 {
274 for (int i = 0; i < this.Workbook.Worksheets.Count; i++)
275 {
276 string fileName = "sheet" + ParserUtils.ToString(i + 1) + ".xml";
277 RegisterPackagePart(PackagePartType.Worksheet, worksheetOrderNumber, fileName, "xl/worksheets", @"application/vnd.openxmlformats-officedocument.spreadsheetml.worksheet+xml", @"http://schemas.openxmlformats.org/officeDocument/2006/relationships/worksheet");
278 worksheetOrderNumber++;
279 }
280 }
281 int postWorksheetOrderNumber = PackagePartDefinition.POST_WORkSHEET_PACKAGE_PART_START_INDEX;
282 if (Workbook.WorkbookTheme != null)
283 {
284 RegisterPackagePart(PackagePartType.Other, postWorksheetOrderNumber, THEME, @"application/vnd.openxmlformats-officedocument.theme+xml", @"http://schemas.openxmlformats.org/officeDocument/2006/relationships/theme");
285 postWorksheetOrderNumber += 1000;
286 }
287 RegisterPackagePart(PackagePartType.Other, postWorksheetOrderNumber, STYLES, @"application/vnd.openxmlformats-officedocument.spreadsheetml.styles+xml", @"http://schemas.openxmlformats.org/officeDocument/2006/relationships/styles");
288 postWorksheetOrderNumber += 1000;
289 RegisterPackagePart(PackagePartType.Other, postWorksheetOrderNumber, SHARED_STRINGS, @"application/vnd.openxmlformats-officedocument.spreadsheetml.sharedStrings+xml", @"http://schemas.openxmlformats.org/officeDocument/2006/relationships/sharedStrings");
290 // TODO: add themeIndex once if media is embedded
291
292 this.Workbook.AuxiliaryData.SetData(PlugInUUID.WriterPackageRegistryQueue, PlugInUUID.LastPackageOrderNumber, postWorksheetOrderNumber, false);
293 }
294
298 private void PreparePackage()
299 {
300 List<PackagePartDefinition> definitions = PackagePartDefinition.Sort(this.packagePartDefinitions);
301 PackagePartDefinition workbookDefinition = definitions.First(p => p.OrderNumber == PackagePartDefinition.WORKBOOK_PACKAGE_PART_INDEX);
302 PackagePart workbookPart = CreateRootPackagePart(workbookDefinition.Path, workbookDefinition.ContentType, workbookDefinition.RelationshipType);
303 foreach (PackagePartDefinition definition in definitions)
304 {
305 if (definition.OrderNumber == PackagePartDefinition.WORKBOOK_PACKAGE_PART_INDEX)
306 {
307 continue;
308 }
309 PackagePart createdPart;
310 string workbookRelationshipId = null;
311 if (definition.PartType == PackagePartType.Root)
312 {
313 createdPart = CreateRootPackagePart(definition.Path, definition.ContentType, definition.RelationshipType);
314 }
315 else
316 {
317 createdPart = CreateXlPackagePart(workbookPart, definition.Path, definition.ContentType, definition.RelationshipType, out workbookRelationshipId);
318 if (definition.PartType == PackagePartType.Worksheet)
319 {
320 worksheetPaths.Add(definition.GetWorksheetIndex(), definition.Path);
321 }
322 }
323 if (definition.UniquePackagePartIndex != null)
324 {
325 queuedPackageParts.Add(definition.UniquePackagePartIndex, createdPart);
326 if (workbookRelationshipId != null)
327 {
328 Workbook.AuxiliaryData.SetData(PlugInUUID.WriterPackageRegistryQueue, PlugInUUID.PackagePartRelationshipId, definition.UniquePackagePartIndex, workbookRelationshipId);
329 }
330 }
331 CreatePackagePartRelationships(createdPart, definition.Relationships);
332 }
333 }
334
340 private static void CreatePackagePartRelationships(PackagePart packagePart, IReadOnlyList<PackagePartRelationshipDefinition> relationships)
341 {
342 foreach (PackagePartRelationshipDefinition relationship in relationships)
343 {
344 bool rootRelativeInternalTarget = relationship.TargetMode == TargetMode.Internal
345 && relationship.Target.StartsWith("/", StringComparison.Ordinal);
346 Uri targetUri = new Uri(relationship.Target, rootRelativeInternalTarget ? UriKind.Relative : UriKind.RelativeOrAbsolute);
347 packagePart.CreateRelationship(targetUri, relationship.TargetMode, relationship.RelationshipType, relationship.RelationshipId);
348 }
349 }
350
358 internal PackagePart CreateRootPackagePart(DocumentPath documentPath, string contentType, string relationshipType)
359 {
360 Uri uri = new Uri(documentPath.GetFullPath(), UriKind.Relative);
361 PackagePart part = this.package.CreatePart(uri, contentType, CompressionOption.Normal);
362 if (!packageParts.ContainsKey(documentPath.Path))
363 {
364 packageParts.Add(documentPath.Path, new Dictionary<string, PackagePart>());
365 }
366 packageParts[documentPath.Path].Add(documentPath.Filename, part);
367 this.package.CreateRelationship(uri, TargetMode.Internal, relationshipType, "rId" + ParserUtils.ToString(rootPackageIndex));
368 rootPackageIndex++;
369 return part;
370 }
371
380 internal PackagePart CreateXlPackagePart(PackagePart parentPart, DocumentPath documentPath, string contentType, string relationshipType, out string relationshipId)
381 {
382 Uri uri = new Uri(documentPath.GetFullPath(), UriKind.Relative);
383 PackagePart part = this.package.CreatePart(uri, contentType, CompressionOption.Normal);
384 if (!packageParts.ContainsKey(documentPath.Path))
385 {
386 packageParts.Add(documentPath.Path, new Dictionary<string, PackagePart>());
387 }
388 packageParts[documentPath.Path].Add(documentPath.Filename, part);
389 relationshipId = "rId" + ParserUtils.ToString(xlPackageIndex);
390 parentPart.CreateRelationship(uri, TargetMode.Internal, relationshipType, relationshipId);
391 xlPackageIndex++;
392 return part;
393 }
394
404 internal void RegisterPackagePart(PackagePartDefinition.PackagePartType type, int orderNumber, string fileNameInPackage, string pathInPackage, string contentType, string relationshipType)
405 {
406 this.packagePartDefinitions.Add(new PackagePartDefinition(type, orderNumber, fileNameInPackage, pathInPackage, contentType, relationshipType));
407 }
408
417 internal void RegisterPackagePart(PackagePartType type, int orderNumber, DocumentPath documentPath, string contentType, string relationshipType)
418 {
419 this.packagePartDefinitions.Add(new PackagePartDefinition(type, orderNumber, documentPath, contentType, relationshipType));
420 }
421
432 private void RegisterPackagePart(PackagePartType type, int orderNumber, DocumentPath documentPath, string contentType, string relationshipType, string uniquePackagePartIndex, IReadOnlyList<IPluginPackageRelationship> relationships)
433 {
434 this.packagePartDefinitions.Add(new PackagePartDefinition(type, orderNumber, documentPath, contentType, relationshipType, uniquePackagePartIndex, relationships));
435 }
436
437 #endregion
438
439 #region interface_methodes
440
445 public void MarkFeatureAsPrepared(string featureUuid)
446 {
447 ValidateFeatureUuid(featureUuid);
448 preparedWriterFeatures.Add(featureUuid);
449 }
450
456 public bool IsFeaturePrepared(string featureUuid)
457 {
458 ValidateFeatureUuid(featureUuid);
459 return preparedWriterFeatures.Contains(featureUuid);
460 }
461
467 private static void ValidateFeatureUuid(string uuid)
468 {
469 if (string.IsNullOrWhiteSpace(uuid))
470 {
471 throw new ArgumentException("The feature UUID must not be null, empty or whitespace");
472 }
473 // TODO add other validation checks here if applicable (e.g. UUID must be officially registered)
474 }
475
476 #endregion
477
478 #region helper_methods
483 private void HandleQueuePlugIns(string queueUuid)
484 {
485 IPlugin queuePlugIn;
486 string lastUuid = null;
487 do
488 {
489 queuePlugIn = PlugInLoader.GetNextQueuePlugIn<IPlugin>(queueUuid, lastUuid, out string currentUuid);
490 if (queuePlugIn != null)
491 {
492 lastUuid = currentUuid;
493 if (!(queuePlugIn is IPluginWriter queueWriter))
494 {
495 continue;
496 }
497 queueWriter.Init(this);
498 if (queueWriter is IPluginIndexedWriter indexedWriter)
499 {
500 HandleIndexedWriter(indexedWriter, queueUuid);
501 }
502 else
503 {
504 queueWriter.Execute();
505 }
506 }
507 else
508 {
509 lastUuid = null;
510 }
511
512 } while (queuePlugIn != null);
513 }
514
520 private void HandleIndexedWriter(IPluginIndexedWriter indexedWriter, string queueUuid)
521 {
522 int maxIndex = indexedWriter.MaxIndex;
523 if (maxIndex < -1)
524 {
525 throw new IOException("Invalid maximum index in indexed writer plug-in: " + indexedWriter.GetType().Name);
526 }
527 if (maxIndex == -1)
528 {
529 return;
530 }
531 if (queueUuid == PlugInUUID.WriterPrependingQueue)
532 {
533 throw new IOException("Indexed writer plug-ins cannot be executed in the writer prepending queue: " + indexedWriter.GetType().Name);
534 }
535
536 for (int i = 0; i <= maxIndex; i++)
537 {
538 indexedWriter.CurrentIndex = i;
539 indexedWriter.Execute();
540 string uniquePackagePartIndex = indexedWriter.CurrentUniquePackagePartIndex;
541 if (uniquePackagePartIndex == null)
542 {
543 continue;
544 }
545 if (string.IsNullOrWhiteSpace(uniquePackagePartIndex))
546 {
547 throw new IOException("Blank package part index in indexed writer plug-in: " + indexedWriter.GetType().Name);
548 }
549 if (!queuedPackageParts.TryGetValue(uniquePackagePartIndex, out PackagePart packagePart))
550 {
551 throw new IOException("Unknown package part index '" + uniquePackagePartIndex + "' in indexed writer plug-in: " + indexedWriter.GetType().Name);
552 }
553 if (indexedWriter.XmlElement == null)
554 {
555 throw new IOException("Missing XML element in indexed writer plug-in: " + indexedWriter.GetType().Name);
556 }
557 AppendXmlToPackagePart(indexedWriter.XmlElement, packagePart);
558 }
559 }
560
564 private void HandlePackageRegistryQueuePlugIns()
565 {
566 IPlugin queuePlugIn;
567 string lastUuid = null;
568 do
569 {
570 queuePlugIn = PlugInLoader.GetNextQueuePlugIn<IPlugin>(PlugInUUID.WriterPackageRegistryQueue, lastUuid, out string currentUuid);
571 if (queuePlugIn != null)
572 {
573 lastUuid = currentUuid;
574 if (!(queuePlugIn is IPluginPackageRegistry packageRegistry))
575 {
576 continue;
577 }
578 packageRegistry.Init(this);
579 packageRegistry.Execute();
580 int counter = ValidatePackageRegistryPlugin(packageRegistry);
581 for (int i = 0; i < counter; i++)
582 {
583 ValidatePackageRegistryEntry(packageRegistry, i);
584 PackagePartType packagePartType = packageRegistry.ArePackagePartsRoot[i] ? PackagePartType.Root : PackagePartType.Other;
585 RegisterPackagePart(
586 packagePartType,
587 packageRegistry.OrderNumbers[i],
588 new DocumentPath(packageRegistry.PackagePartFileNames[i], packageRegistry.PackagePartPaths[i]),
589 packageRegistry.ContentTypes[i],
590 packageRegistry.RelationshipTypes[i],
591 packageRegistry.UniquePackagePartIndices[i],
592 packageRegistry.PackagePartRelationships[i]);
593 }
594 }
595 else
596 {
597 lastUuid = null;
598 }
599
600 } while (queuePlugIn != null);
601 }
602
608 private static int ValidatePackageRegistryPlugin(IPluginPackageRegistry plugin)
609 {
610 if (plugin.OrderNumbers == null ||
611 plugin.ArePackagePartsRoot == null ||
612 plugin.ContentTypes == null ||
613 plugin.PackagePartFileNames == null ||
614 plugin.PackagePartPaths == null ||
615 plugin.RelationshipTypes == null ||
616 plugin.UniquePackagePartIndices == null ||
617 plugin.PackagePartRelationships == null)
618 {
619 throw new IOException("Null collection in package registry plug-in: " + plugin.GetType().Name);
620 }
621
622 int count = plugin.OrderNumbers.Count;
623 if (plugin.ArePackagePartsRoot.Count != count ||
624 plugin.ContentTypes.Count != count ||
625 plugin.PackagePartFileNames.Count != count ||
626 plugin.PackagePartPaths.Count != count ||
627 plugin.RelationshipTypes.Count != count ||
628 plugin.UniquePackagePartIndices.Count != count ||
629 plugin.PackagePartRelationships.Count != count)
630 {
631 throw new IOException("Inconsistent package registry plug-in detected: " + plugin.GetType().Name);
632 }
633 return count;
634 }
635
641 private void ValidatePackageRegistryEntry(IPluginPackageRegistry plugin, int index)
642 {
643 string uniquePackagePartIndex = plugin.UniquePackagePartIndices[index];
644 if (string.IsNullOrWhiteSpace(uniquePackagePartIndex))
645 {
646 throw new IOException("Blank package part index in package registry plug-in: " + plugin.GetType().Name);
647 }
648 if (packagePartDefinitions.Any(definition => string.Equals(definition.UniquePackagePartIndex, uniquePackagePartIndex, StringComparison.Ordinal)))
649 {
650 throw new IOException("Duplicate package part index '" + uniquePackagePartIndex + "' in package registry plug-in: " + plugin.GetType().Name);
651 }
652 if (string.IsNullOrWhiteSpace(plugin.PackagePartPaths[index]) ||
653 string.IsNullOrWhiteSpace(plugin.PackagePartFileNames[index]) ||
654 string.IsNullOrWhiteSpace(plugin.ContentTypes[index]) ||
655 string.IsNullOrWhiteSpace(plugin.RelationshipTypes[index]))
656 {
657 throw new IOException("Invalid package part definition in package registry plug-in: " + plugin.GetType().Name);
658 }
659 ValidatePackagePartRelationships(plugin, index);
660 }
661
667 private static void ValidatePackagePartRelationships(IPluginPackageRegistry plugin, int index)
668 {
669 IReadOnlyList<IPluginPackageRelationship> relationships = plugin.PackagePartRelationships[index];
670 if (relationships == null)
671 {
672 throw new IOException("Null package relationship collection in package registry plug-in: " + plugin.GetType().Name);
673 }
674
675 HashSet<string> relationshipIds = new HashSet<string>(StringComparer.Ordinal);
676 foreach (IPluginPackageRelationship relationship in relationships)
677 {
678 ValidatePackagePartRelationship(plugin, relationship, relationshipIds);
679 }
680 }
681
688 private static void ValidatePackagePartRelationship(IPluginPackageRegistry plugin, IPluginPackageRelationship relationship, HashSet<string> relationshipIds)
689 {
690 if (relationship == null)
691 {
692 throw new IOException("Null package relationship in package registry plug-in: " + plugin.GetType().Name);
693 }
694 if (string.IsNullOrWhiteSpace(relationship.RelationshipId))
695 {
696 throw new IOException("Blank package relationship ID in package registry plug-in: " + plugin.GetType().Name);
697 }
698 try
699 {
700 XmlConvert.VerifyNCName(relationship.RelationshipId);
701 }
702 catch (XmlException)
703 {
704 throw new IOException("Invalid package relationship ID '" + relationship.RelationshipId + "' in package registry plug-in: " + plugin.GetType().Name);
705 }
706 if (!relationshipIds.Add(relationship.RelationshipId))
707 {
708 throw new IOException("Duplicate package relationship ID '" + relationship.RelationshipId + "' in package registry plug-in: " + plugin.GetType().Name);
709 }
710 if (string.IsNullOrWhiteSpace(relationship.RelationshipType) ||
711 !Uri.TryCreate(relationship.RelationshipType, UriKind.Absolute, out _)) // Check URI of type, not target
712 {
713 throw new IOException("Invalid package relationship type in package registry plug-in: " + plugin.GetType().Name);
714 }
715 if (string.IsNullOrWhiteSpace(relationship.Target) ||
716 !Uri.TryCreate(relationship.Target, UriKind.RelativeOrAbsolute, out Uri targetUri)) // Check URI of target
717 {
718 throw new IOException("Invalid package relationship target in package registry plug-in: " + plugin.GetType().Name);
719 }
720 if (relationship.TargetMode != TargetMode.Internal && relationship.TargetMode != TargetMode.External)
721 {
722 throw new IOException("Invalid package relationship target mode in package registry plug-in: " + plugin.GetType().Name);
723 }
724 bool rootRelativeTarget = relationship.Target.StartsWith("/", StringComparison.Ordinal);
725 if (relationship.TargetMode == TargetMode.Internal && relationship.Target.StartsWith("//", StringComparison.Ordinal))
726 {
727 throw new IOException("Network-path internal package relationship target in package registry plug-in: " + plugin.GetType().Name);
728 }
729 if (relationship.TargetMode == TargetMode.Internal && targetUri.IsAbsoluteUri && !rootRelativeTarget)
730 {
731 throw new IOException("Absolute internal package relationship target in package registry plug-in: " + plugin.GetType().Name);
732 }
733 }
734
740 private static void AppendXmlToPackagePart(XmlElement rootElement, PackagePart pp)
741 {
742 using (MemoryStream ms = new MemoryStream())
743 {
744 XmlWriterSettings settings = new XmlWriterSettings
745 {
746 Encoding = new UTF8Encoding(false), // No BOM
747 Indent = true,
748 OmitXmlDeclaration = true, // Include <?xml version="1.0" encoding="utf-8"?>
749 CloseOutput = false
750 };
751
752 using (XmlWriter writer = XmlWriter.Create(ms, settings))
753 {
754 writer.WriteProcessingInstruction("xml", "version=\"1.0\" encoding=\"UTF-8\" standalone=\"yes\"");
755 rootElement.WriteTo(writer);
756 writer.Flush();
757 }
758
759 AddStreamToPackagePart(ms, pp);
760 }
761 }
762
768 internal static void AddStreamToPackagePart(MemoryStream stream, PackagePart pp)
769 {
770 stream.Position = 0;
771 stream.CopyTo(pp.GetStream());
772 stream.Flush();
773 }
774
775 #endregion
776 }
777}