NanoXLSX.Reader 3.2.1
Loading...
Searching...
No Matches
DiscoveryReader.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 System;
9using System.Collections.Generic;
10using System.IO.Compression;
11using System.IO.Packaging;
12using System.Linq;
13using System.Xml;
14using NanoXLSX.Interfaces;
15using NanoXLSX.Interfaces.Reader;
16using NanoXLSX.Registry;
17using NanoXLSX.Registry.Attributes;
18using NanoXLSX.Utils.Xml;
19using IOException = NanoXLSX.Exceptions.IOException;
20
22{
26 [NanoXlsxPlugIn(PlugInUUID = PlugInUUID.DiscoveryReader)]
27 public class DiscoveryReader : IDiscoveryReader
28 {
29 private const string RELATIONSHIPS_NAMESPACE = "http://schemas.openxmlformats.org/package/2006/relationships";
30 private ZipArchive archive;
31
35 public Workbook Workbook { get; set; }
36
40 public IOptions Options { get; set; }
41
46 {
47 }
48
52 public void Init(ZipArchive archive, Workbook workbook, IOptions readerOptions)
53 {
54 this.archive = archive;
55 Workbook = workbook;
56 Options = readerOptions;
57 }
58
63 public void Execute()
64 {
65 if (archive == null)
66 {
67 throw new IOException("The discovery reader was not initialized with a ZIP archive.");
68 }
69 if (Workbook == null)
70 {
71 throw new IOException("The discovery reader was not initialized with a workbook.");
72 }
73 RelationshipCatalog catalog = DiscoverRelationships();
74 Workbook.AuxiliaryData.SetData(PlugInUUID.DiscoveryReader, PlugInUUID.DiscoveryCatalogEntity, catalog);
75 }
76
81 private RelationshipCatalog DiscoverRelationships()
82 {
83 RelationshipCatalog catalog = new RelationshipCatalog();
84 List<ZipArchiveEntry> relationshipEntries = archive.Entries
85 .Where(entry => IsPotentialRelationshipPart(entry.FullName))
86 .OrderBy(entry => entry.FullName, StringComparer.Ordinal)
87 .ToList();
88
89 foreach (ZipArchiveEntry entry in relationshipEntries)
90 {
91 if (!TryGetSourcePartPath(entry.FullName, out string sourcePartPath, out string pathError))
92 {
93 HandlePartIssue(catalog, entry.FullName, pathError, null);
94 continue;
95 }
96 DiscoverRelationshipPart(entry, sourcePartPath, catalog);
97 }
98 return catalog;
99 }
100
108 private void DiscoverRelationshipPart(ZipArchiveEntry entry, string sourcePartPath, RelationshipCatalog catalog)
109 {
110 List<RelationshipInfo> partRelationships = new List<RelationshipInfo>();
111 List<RelationshipDiscoveryIssue> partIssues = new List<RelationshipDiscoveryIssue>();
112 HashSet<string> relationshipIds = new HashSet<string>(StringComparer.Ordinal);
113 try
114 {
115 using (System.IO.Stream stream = entry.Open())
116 using (XmlReader reader = XmlReader.Create(stream, XmlStreamUtils.CreateSettings()))
117 {
118 bool rootFound = false;
119 while (reader.Read())
120 {
121 if (reader.NodeType != XmlNodeType.Element)
122 {
123 continue;
124 }
125 if (!rootFound)
126 {
127 if (reader.Depth != 0 || reader.LocalName != "Relationships" || reader.NamespaceURI != RELATIONSHIPS_NAMESPACE)
128 {
129 throw new XmlException("The relationship part has an invalid root element.");
130 }
131 rootFound = true;
132 continue;
133 }
134 if (reader.Depth != 1 || reader.LocalName != "Relationship" || reader.NamespaceURI != RELATIONSHIPS_NAMESPACE)
135 {
136 throw new XmlException("The relationship part contains an invalid element.");
137 }
138
139 RelationshipInfo relationship = ParseRelationship(reader, entry.FullName, sourcePartPath, partIssues);
140 if (relationship == null) // Error case (issues are collected, skip)
141 {
142 continue;
143 }
144 if (!relationshipIds.Add(relationship.Id))
145 {
146 string reason = "The relationship identifier is duplicated within its source part.";
147 if (IsStrictValidation)
148 {
149 throw CreateDiscoveryException(entry.FullName, relationship.Id, reason, null); // Results in a IOException
150 }
151 partIssues.Add(new RelationshipDiscoveryIssue(entry.FullName, relationship.Id, reason));
152 continue;
153 }
154 partRelationships.Add(relationship);
155 }
156 }
157 }
158 catch (IOException)
159 {
160 throw; // Re-throw
161 }
162 catch (Exception ex)
163 {
164 HandlePartIssue(catalog, entry.FullName, "The relationship part could not be parsed.", ex);
165 return;
166 }
167
168 foreach (RelationshipInfo relationship in partRelationships)
169 {
170 if (!catalog.TryAdd(relationship))
171 {
172 string reason = "The relationship identifier is duplicated within its source part.";
173 if (IsStrictValidation)
174 {
175 throw CreateDiscoveryException(entry.FullName, relationship.Id, reason, null); // Results in a IOException
176 }
177 catalog.AddIssue(new RelationshipDiscoveryIssue(entry.FullName, relationship.Id, reason));
178 }
179 }
180 foreach (RelationshipDiscoveryIssue issue in partIssues)
181 {
182 catalog.AddIssue(issue);
183 }
184 }
185
195 private RelationshipInfo ParseRelationship(XmlReader reader, string relationshipPartPath, string sourcePartPath, List<RelationshipDiscoveryIssue> issues)
196 {
197 string id = reader.GetAttribute("Id");
198 string type = reader.GetAttribute("Type");
199 string target = reader.GetAttribute("Target");
200 string targetModeValue = reader.GetAttribute("TargetMode");
201 string reason = ValidateRequiredAttributes(id, type, target);
202 if (reason != null)
203 {
204 return HandleRelationshipIssue(relationshipPartPath, id, reason, issues, null);
205 }
206
207 try
208 {
209 XmlConvert.VerifyNCName(id);
210 }
211 catch (Exception ex)
212 {
213 return HandleRelationshipIssue(relationshipPartPath, id, "The relationship identifier is not a valid XML NCName.", issues, ex);
214 }
215 if (!Uri.TryCreate(type, UriKind.Absolute, out Uri unusedTypeUri))
216 {
217 return HandleRelationshipIssue(relationshipPartPath, id, "The relationship type is not an absolute URI.", issues, null);
218 }
219 if (!Uri.TryCreate(target, UriKind.RelativeOrAbsolute, out Uri targetUri))
220 {
221 return HandleRelationshipIssue(relationshipPartPath, id, "The relationship target is not a valid URI reference.", issues, null);
222 }
223
224 TargetMode targetMode;
225 if (string.IsNullOrEmpty(targetModeValue) || targetModeValue == "Internal")
226 {
227 targetMode = TargetMode.Internal;
228 }
229 else if (targetModeValue == "External")
230 {
231 targetMode = TargetMode.External;
232 }
233 else
234 {
235 return HandleRelationshipIssue(relationshipPartPath, id, "The relationship TargetMode is invalid.", issues, null);
236 }
237
238 string resolvedTargetPath = null;
239 if (targetMode == TargetMode.Internal)
240 {
241 bool rootRelativeTarget = target.StartsWith("/", StringComparison.Ordinal);
242 if (target.StartsWith("//", StringComparison.Ordinal))
243 {
244 return HandleRelationshipIssue(relationshipPartPath, id, "An internal relationship target cannot be a network-path reference.", issues, null);
245 }
246 if (targetUri.IsAbsoluteUri && !rootRelativeTarget)
247 {
248 return HandleRelationshipIssue(relationshipPartPath, id, "An internal relationship target cannot be an absolute URI.", issues, null);
249 }
250 if (rootRelativeTarget)
251 {
252 targetUri = new Uri(target, UriKind.Relative);
253 }
254 Uri sourceUri = new Uri("/" + sourcePartPath, UriKind.Relative);
255 Uri resolvedTargetUri = PackUriHelper.ResolvePartUri(sourceUri, targetUri);
256 resolvedTargetPath = resolvedTargetUri.OriginalString.TrimStart('/');
257 }
258
259 return new RelationshipInfo(id, type, target, targetMode, relationshipPartPath, sourcePartPath, resolvedTargetPath);
260 }
261
271 private RelationshipInfo HandleRelationshipIssue(string relationshipPartPath, string relationshipId, string reason, List<RelationshipDiscoveryIssue> issues, Exception innerException)
272 {
273 if (IsStrictValidation)
274 {
275 throw CreateDiscoveryException(relationshipPartPath, relationshipId, reason, innerException);
276 }
277 issues.Add(new RelationshipDiscoveryIssue(relationshipPartPath, relationshipId, reason));
278 return null;
279 }
280
288 private void HandlePartIssue(RelationshipCatalog catalog, string relationshipPartPath, string reason, Exception innerException)
289 {
290 if (IsStrictValidation)
291 {
292 throw CreateDiscoveryException(relationshipPartPath, null, reason, innerException);
293 }
294 catalog.AddIssue(new RelationshipDiscoveryIssue(relationshipPartPath, null, reason));
295 }
296
304 private static string ValidateRequiredAttributes(string id, string type, string target)
305 {
306 if (string.IsNullOrWhiteSpace(id))
307 {
308 return "The relationship Id attribute is missing or empty.";
309 }
310 if (string.IsNullOrWhiteSpace(type))
311 {
312 return "The relationship Type attribute is missing or empty.";
313 }
314 if (string.IsNullOrWhiteSpace(target))
315 {
316 return "The relationship Target attribute is missing or empty.";
317 }
318 return null;
319 }
320
326 private static bool IsPotentialRelationshipPart(string path)
327 {
328 return !string.IsNullOrEmpty(path)
329 && path.EndsWith(".rels", StringComparison.OrdinalIgnoreCase)
330 && (path.StartsWith("_rels/", StringComparison.Ordinal) || path.IndexOf("/_rels/", StringComparison.Ordinal) >= 0);
331 }
332
340 private static bool TryGetSourcePartPath(string relationshipPartPath, out string sourcePartPath, out string error)
341 {
342 sourcePartPath = null;
343 error = null;
344 try
345 {
346 Uri relationshipPartUri = PackUriHelper.CreatePartUri(new Uri("/" + relationshipPartPath, UriKind.Relative));
347 if (!PackUriHelper.IsRelationshipPartUri(relationshipPartUri)
348 || !string.Equals(relationshipPartUri.OriginalString.TrimStart('/'), relationshipPartPath, StringComparison.Ordinal))
349 {
350 error = "The ZIP entry path is not a valid OPC relationship-part path.";
351 return false;
352 }
353 Uri sourcePartUri = PackUriHelper.GetSourcePartUriFromRelationshipPartUri(relationshipPartUri);
354 sourcePartPath = sourcePartUri.OriginalString.TrimStart('/');
355 return true;
356 }
357 catch (Exception ex)
358 {
359 error = "The ZIP entry path is not a valid OPC relationship-part path: " + ex.Message;
360 return false;
361 }
362 }
363
372 private static IOException CreateDiscoveryException(string relationshipPartPath, string relationshipId, string reason, Exception innerException)
373 {
374 string relationshipContext = relationshipId == null ? string.Empty : " (Id '" + relationshipId + "')";
375 string message = "The relationship part '" + relationshipPartPath + "'" + relationshipContext + " is invalid. " + reason;
376 return innerException == null ? new IOException(message) : new IOException(message, innerException);
377 }
378
383 private bool IsStrictValidation
384 {
385 get
386 {
387 ReaderOptions readerOptions = Options as ReaderOptions;
388 return readerOptions != null && readerOptions.EnforceStrictValidation;
389 }
390 }
391 }
392}
void Execute()
Discovers all valid relationship parts and prepares the temporary relationship catalog.
IOptions Options
Gets or sets the reader options used for discovery validation.
DiscoveryReader()
Initializes a new discovery reader.
void Init(ZipArchive archive, Workbook workbook, IOptions readerOptions)
Initializes discovery with a caller-owned ZIP archive.
Workbook Workbook
Gets or sets the workbook that receives the temporary discovery catalog.
Exceptions.IOException IOException