Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
105 changes: 105 additions & 0 deletions src/DocumentFormat.OpenXml.Framework/Features/StreamPackageFeature.cs
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand Down Expand Up @@ -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)
Expand Down Expand Up @@ -170,6 +174,107 @@ public void Dispose()
GC.SuppressFinalize(this);
}

// Some OOXML producers omit the required <Default Extension="rels" .../> 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\"";

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Technically, the extension is not required to be rels. Instead check for application/vnd.openxmlformats-package.relationships+xml because the content type won't change

const string Closing = "</Types>";
const string Injection =
"<Default Extension=\"rels\" " +
"ContentType=\"application/vnd.openxmlformats-package.relationships+xml\"/>";

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

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Instead of checking for the element and adding it if it's missing, check for ctXml.IndexOf(Marker, StringComparison.Ordinal) == 0 and throw an exception if it is missing. This parallels Office behavior and avoids adding preprocessor statements.

if (ctXml.Contains(Marker, StringComparison.Ordinal))

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

hey @rakeshbabuseva,

Thanks very much for this PR. I think this is a good catch. I do have some thoughts on this and please take a look and see what you think.

  1. I think to be more correct, and although this is probably hitting corner cases, the code should be testing on "ContentType=\"application/vnd.openxmlformats-package.relationships+xml\"/>" instead of "<Default Extension=\"rels\" "

  2. If it does not find any <Default Extension=...> child with application/vnd.openxmlformats-package.relationships+xml content type, it should throw an exception as this likely would mean the integrity of the package is not sound.

  3. If it does find such a <Default Extension...> child then the package should be considered ok.

Adding "<Default Extension=\"rels\" " + "ContentType=\"application/vnd.openxmlformats-package.relationships+xml\"/>" would probably be correct most of the time but I think 29500-1 and -2 allow for using another extension and that could mean we're not helping the integrity of the package.

Word does call this out to the user and while it does offer to repair and will use "<Default Extension=\"rels\", Word also would have the extra logic to make sure the actual relationship parts use that extension or change them if they don't. We probably don't want to put all that logic in the SDK.

Finally, I think that throwing the exception would be good since it's apparent that the package integrity is not good and this would allow the Open XML producing app, library or tool to correct the situation rather than mask it.

What do you think? We can discuss further if you'd like.

{
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);
Expand Down
65 changes: 65 additions & 0 deletions test/DocumentFormat.OpenXml.Packaging.Tests/OpenXmlPackageTests.cs
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand Down Expand Up @@ -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", """
<?xml version="1.0" encoding="UTF-8" standalone="yes"?>
<Types xmlns="http://schemas.openxmlformats.org/package/2006/content-types">
<Default Extension="xml" ContentType="application/xml"/>
<Override PartName="/word/document.xml"
ContentType="application/vnd.openxmlformats-officedocument.wordprocessingml.document.main+xml"/>
</Types>
""");
Write(zip, "_rels/.rels", """
<?xml version="1.0" encoding="UTF-8" standalone="yes"?>
<Relationships xmlns="http://schemas.openxmlformats.org/package/2006/relationships">
<Relationship Id="rId1"
Type="http://schemas.openxmlformats.org/officeDocument/2006/relationships/officeDocument"
Target="word/document.xml"/>
</Relationships>
""");
Write(zip, "word/document.xml", """
<?xml version="1.0" encoding="UTF-8" standalone="yes"?>
<w:document xmlns:w="http://schemas.openxmlformats.org/wordprocessingml/2006/main">
<w:body><w:p/></w:body>
</w:document>
""");
}

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);
}
}
}
}
Loading