diff --git a/src/DocumentFormat.OpenXml.Framework/Features/StreamPackageFeature.cs b/src/DocumentFormat.OpenXml.Framework/Features/StreamPackageFeature.cs
index 2edb9a890..a6fde6c31 100644
--- a/src/DocumentFormat.OpenXml.Framework/Features/StreamPackageFeature.cs
+++ b/src/DocumentFormat.OpenXml.Framework/Features/StreamPackageFeature.cs
@@ -7,6 +7,9 @@
using System;
using System.Diagnostics.CodeAnalysis;
using System.IO;
+#if !NETFRAMEWORK
+using System.IO.Compression;
+#endif
using System.IO.Packaging;
namespace DocumentFormat.OpenXml.Features;
@@ -128,6 +131,7 @@ private void InitializePackage(FileMode? mode = default, FileAccess? access = de
try
{
+ _stream = RepairRelsContentType(_stream);
_package = Package.Open(_stream, mode.Value, access.Value);
}
catch (ArgumentException ex)
@@ -170,6 +174,107 @@ public void Dispose()
GC.SuppressFinalize(this);
}
+ // Some OOXML producers omit the required entry from
+ // [Content_Types].xml (a known OPC spec deviation). System.IO.Packaging is strict: without
+ // that registration it cannot resolve .rels parts, so package-level relationships are never
+ // loaded and MainDocumentPart (and equivalents) come back null. Repair the stream in-memory
+ // before handing it to Package.Open so the SDK behaves like Word Desktop and the MIP SDK,
+ // both of which recover silently from this deviation.
+ //
+ // Gated on !NETFRAMEWORK because ZipArchive requires net45+ and the old System.IO.Packaging
+ // on net35/net40/net46 is already lenient about missing content-type registrations.
+#if !NETFRAMEWORK
+ private static Stream RepairRelsContentType(Stream input)
+ {
+ const string Marker = "Extension=\"rels\"";
+ const string Closing = "";
+ const string Injection =
+ "";
+
+ if (!input.CanRead || !input.CanSeek)
+ {
+ return input;
+ }
+
+ long originalPosition = input.Position;
+
+ try
+ {
+ string ctXml;
+ using (var readZip = new ZipArchive(input, ZipArchiveMode.Read, leaveOpen: true))
+ {
+ var ctEntry = readZip.GetEntry("[Content_Types].xml");
+ if (ctEntry is null)
+ {
+ input.Position = originalPosition;
+ return input;
+ }
+
+ using var reader = new StreamReader(ctEntry.Open());
+ ctXml = reader.ReadToEnd();
+ }
+
+#if NET6_0_OR_GREATER
+ if (ctXml.Contains(Marker, StringComparison.Ordinal))
+ {
+ input.Position = originalPosition;
+ return input;
+ }
+
+ int closingIdx = ctXml.IndexOf(Closing, StringComparison.Ordinal);
+ string patched = closingIdx >= 0
+ ? string.Concat(ctXml.AsSpan(0, closingIdx), Injection, ctXml.AsSpan(closingIdx))
+ : ctXml + Injection;
+#else
+ if (ctXml.IndexOf(Marker, StringComparison.Ordinal) >= 0)
+ {
+ input.Position = originalPosition;
+ return input;
+ }
+
+ int closingIdx = ctXml.IndexOf(Closing, StringComparison.Ordinal);
+ string patched = closingIdx >= 0
+ ? ctXml.Substring(0, closingIdx) + Injection + ctXml.Substring(closingIdx)
+ : ctXml + Injection;
+#endif
+
+ input.Position = originalPosition;
+ var output = new MemoryStream();
+ using (var inZip = new ZipArchive(input, ZipArchiveMode.Read, leaveOpen: false))
+ using (var outZip = new ZipArchive(output, ZipArchiveMode.Create, leaveOpen: true))
+ {
+ foreach (var entry in inZip.Entries)
+ {
+ var outEntry = outZip.CreateEntry(entry.FullName, CompressionLevel.Fastest);
+ outEntry.LastWriteTime = entry.LastWriteTime;
+ using var inStream = entry.Open();
+ using var outStream = outEntry.Open();
+ if (entry.FullName == "[Content_Types].xml")
+ {
+ using var sw = new StreamWriter(outStream);
+ sw.Write(patched);
+ }
+ else
+ {
+ inStream.CopyTo(outStream);
+ }
+ }
+ }
+
+ output.Position = 0;
+ return output;
+ }
+ catch
+ {
+ input.Position = originalPosition;
+ return input;
+ }
+ }
+#else
+ private static Stream RepairRelsContentType(Stream input) => input;
+#endif
+
protected override void Register(IFeatureCollection features)
{
base.Register(features);
diff --git a/test/DocumentFormat.OpenXml.Packaging.Tests/OpenXmlPackageTests.cs b/test/DocumentFormat.OpenXml.Packaging.Tests/OpenXmlPackageTests.cs
index f1b957bd8..159122cb1 100644
--- a/test/DocumentFormat.OpenXml.Packaging.Tests/OpenXmlPackageTests.cs
+++ b/test/DocumentFormat.OpenXml.Packaging.Tests/OpenXmlPackageTests.cs
@@ -6,6 +6,7 @@
using System;
using System.Collections.Generic;
using System.IO;
+using System.IO.Compression;
using System.Linq;
using System.Xml.Linq;
using Xunit;
@@ -398,5 +399,69 @@ public void IsEncryptedOfficeFile_ReturnsFalse_ForUnencryptedFile_FromString()
// Clean up the test file path
File.Delete(filePath);
}
+
+ [Fact]
+ public void Open_MissingRelsContentType_MainDocumentPartIsNotNull()
+ {
+ using var stream = CreateDocxMissingRelsContentType();
+ using var doc = WordprocessingDocument.Open(stream, isEditable: false);
+ Assert.NotNull(doc.MainDocumentPart);
+ }
+
+ [Fact]
+ public void Open_MissingRelsContentType_IsIdempotent()
+ {
+ // Opening a file that already has the rels Default entry must not alter it.
+ var wellFormed = new MemoryStream();
+ using (var doc = WordprocessingDocument.Create(wellFormed, WordprocessingDocumentType.Document, autoSave: false))
+ {
+ var main = doc.AddMainDocumentPart();
+ main.Document = new Document(new Body(new Paragraph()));
+ doc.Save();
+ }
+
+ wellFormed.Position = 0;
+ using var reopened = WordprocessingDocument.Open(wellFormed, isEditable: false);
+ Assert.NotNull(reopened.MainDocumentPart);
+ }
+
+ private static MemoryStream CreateDocxMissingRelsContentType()
+ {
+ var stream = new MemoryStream();
+ using (var zip = new ZipArchive(stream, ZipArchiveMode.Create, leaveOpen: true))
+ {
+ Write(zip, "[Content_Types].xml", """
+
+
+
+
+
+ """);
+ Write(zip, "_rels/.rels", """
+
+
+
+
+ """);
+ Write(zip, "word/document.xml", """
+
+
+
+
+ """);
+ }
+
+ stream.Position = 0;
+ return stream;
+
+ static void Write(ZipArchive zip, string name, string content)
+ {
+ using var w = new StreamWriter(zip.CreateEntry(name).Open());
+ w.Write(content);
+ }
+ }
}
}