Skip to content

Commit 4f67359

Browse files
authored
Check for seekable stream for malformed uri handling (#1644)
- We must be able to seek to create a temporary file requried for uri handling - We should throw the same exception we did in v2.x - If compat mode is on, this will throw at open time (i.e. it will eagerly load everything) Fixes #1636
1 parent 3e35208 commit 4f67359

File tree

11 files changed

+183
-14
lines changed

11 files changed

+183
-14
lines changed

CHANGELOG.md

Lines changed: 6 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,17 +1,21 @@
11
# Changelog
2+
23
All notable changes to this project will be documented in this file.
34

45
The format is based on [Keep a Changelog](http://keepachangelog.com/en/1.0.0/)
56
and this project adheres to [Semantic Versioning](http://semver.org/spec/v2.0.0.html).
67

7-
## [3.1.0]
8+
## [3.0.1]
89

910
### Fixed
11+
1012
- Fixed issue where document type would not be correct unless content type was checked first (#1625)
13+
- Added check to only seek on packages where it is supported (#1644)
14+
- If a malformed URI is encountered, the exception is now the same as v2.x (`OpenXmlPackageException` with an inner `UriFormatException`) (#1644)
1115

1216
## [3.0.0] - 2023-11-15
1317

14-
## Added
18+
### Added
1519

1620
- `DocumentFormat.OpenXml.Office.PowerPoint.Y2023.M02.Main` namespace
1721
- `DocumentFormat.OpenXml.Office.PowerPoint.Y2022.M03.Main` namespace

src/DocumentFormat.OpenXml.Framework/Features/PackageFeatureBase.cs

Lines changed: 10 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -243,7 +243,16 @@ protected override void Delete(string id)
243243
=> Feature.Package.GetPart(Uri).DeleteRelationship(id);
244244

245245
protected override IEnumerable<PackageRelationship> GetRelationships()
246-
=> Feature.Package.GetPart(Uri).GetRelationships();
246+
{
247+
try
248+
{
249+
return Feature.Package.GetPart(Uri).GetRelationships();
250+
}
251+
catch (UriFormatException ex)
252+
{
253+
throw new OpenXmlPackageException(ExceptionMessages.MalformedUri, ex);
254+
}
255+
}
247256
}
248257

249258
private abstract class RelationshipCollection : IRelationshipCollection

src/DocumentFormat.OpenXml.Framework/Features/StreamPackageFeature.cs

Lines changed: 8 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -111,7 +111,14 @@ private void InitializePackage(FileMode? mode = default, FileAccess? access = de
111111
SetStream(GetStream(mode.Value, access.Value));
112112
}
113113

114-
_package = Package.Open(_stream, mode.Value, access.Value);
114+
try
115+
{
116+
_package = Package.Open(_stream, mode.Value, access.Value);
117+
}
118+
catch (ArgumentException ex)
119+
{
120+
throw new OpenXmlPackageException(ExceptionMessages.FailedToOpenPackage, ex);
121+
}
115122
}
116123

117124
protected override Package Package => _package;

src/DocumentFormat.OpenXml.Framework/Packaging/WriteableStreamExtensions.cs

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -15,7 +15,7 @@ internal static class WriteableStreamExtensions
1515
[System.Diagnostics.CodeAnalysis.SuppressMessage("Reliability", "CA2000:Dispose objects before losing scope", Justification = "Disposable is registered with package")]
1616
public static bool EnableWriteableStream(this IFeatureCollection features)
1717
{
18-
if (features.Get<IPackageStreamFeature>() is { Stream.CanWrite: false } feature &&
18+
if (features.Get<IPackageStreamFeature>() is { Stream: { CanWrite: false, CanSeek: true } } feature &&
1919
features.Get<IPackageFeature>() is { } packageFeature &&
2020
packageFeature.Capabilities.HasFlagFast(PackageCapabilities.Reload))
2121
{

src/DocumentFormat.OpenXml.Framework/Resources/ExceptionMessages.Designer.cs

Lines changed: 18 additions & 0 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

src/DocumentFormat.OpenXml.Framework/Resources/ExceptionMessages.resx

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -408,4 +408,10 @@
408408
<data name="NamespaceIdNotAvailable" xml:space="preserve">
409409
<value>Namespace ids are only available for namespaces for Office 2021 and before. Please use prefixes or URLs instead.</value>
410410
</data>
411+
<data name="MalformedUri" xml:space="preserve">
412+
<value>The package contains malformed URI relationships. Please ensure the package is opened with a stream (that is seekable) or a file path to have the SDK automatically handle these scenarios.</value>
413+
</data>
414+
<data name="FailedToOpenPackage" xml:space="preserve">
415+
<value>Package could not be opened for stream. See inner exception for details and be aware that there are behavior differences in stream support between .NET Framework and Core.</value>
416+
</data>
411417
</root>

test/DocumentFormat.OpenXml.Framework.Tests/DocumentFormat.OpenXml.Framework.Tests.csproj

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -21,6 +21,7 @@
2121
<ItemGroup>
2222
<ProjectReference Include="..\..\src\DocumentFormat.OpenXml.Framework\DocumentFormat.OpenXml.Framework.csproj" />
2323
<ProjectReference Include="..\..\src\DocumentFormat.OpenXml\DocumentFormat.OpenXml.csproj" />
24+
<ProjectReference Include="..\DocumentFormat.OpenXml.Tests.Assets\DocumentFormat.OpenXml.Tests.Assets.csproj" />
2425
</ItemGroup>
2526

2627
</Project>

test/DocumentFormat.OpenXml.Tests.Assets/IFile.cs

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -11,5 +11,9 @@ public interface IFile : IDisposable
1111
string Path { get; }
1212

1313
Stream Open();
14+
15+
bool IsEditable { get; }
16+
17+
FileAccess Access { get; }
1418
}
1519
}

test/DocumentFormat.OpenXml.Tests.Assets/TemporaryFile.cs

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -16,6 +16,10 @@ private TemporaryFile(string path)
1616

1717
public string Path { get; }
1818

19+
public bool IsEditable => true;
20+
21+
public FileAccess Access => FileAccess.ReadWrite;
22+
1923
public Stream Open() => File.Open(Path, FileMode.OpenOrCreate, FileAccess.ReadWrite);
2024

2125
public void Dispose()

test/DocumentFormat.OpenXml.Tests.Assets/TestAssets.cs

Lines changed: 12 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -98,6 +98,10 @@ public MemoryFile(Stream stream, string path)
9898

9999
public string Path { get; }
100100

101+
public bool IsEditable => _stream.CanWrite;
102+
103+
public FileAccess Access => _stream.CanWrite ? FileAccess.ReadWrite : FileAccess.Read;
104+
101105
public Stream Open() => _stream;
102106

103107
public void Dispose()
@@ -108,23 +112,22 @@ public void Dispose()
108112

109113
private class CopiedFile : IFile
110114
{
111-
private readonly FileAccess _access;
112-
113115
public CopiedFile(Stream stream, string extension, FileAccess access)
114116
{
115117
Path = System.IO.Path.Combine(System.IO.Path.GetTempPath(), $"{Guid.NewGuid()}{extension}");
118+
Access = access;
116119

117-
_access = access;
118-
119-
using (var fs = File.OpenWrite(Path))
120-
{
121-
stream.CopyTo(fs);
122-
}
120+
using var fs = File.OpenWrite(Path);
121+
stream.CopyTo(fs);
123122
}
124123

125124
public string Path { get; }
126125

127-
public Stream Open() => File.Open(Path, FileMode.Open, _access);
126+
public bool IsEditable => Access == FileAccess.ReadWrite;
127+
128+
public FileAccess Access { get; }
129+
130+
public Stream Open() => File.Open(Path, FileMode.Open, Access);
128131

129132
public void Dispose()
130133
{

test/DocumentFormat.OpenXml.Tests/OpenXmlDomTest/DocumentOpenTests.cs

Lines changed: 113 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,14 +1,18 @@
11
// Copyright (c) Microsoft. All rights reserved.
22
// Licensed under the MIT license. See LICENSE file in the project root for full license information.
33

4+
using DocumentFormat.OpenXml.Framework;
45
using DocumentFormat.OpenXml.Packaging;
56
using DocumentFormat.OpenXml.Spreadsheet;
67
using DocumentFormat.OpenXml.Validation;
78
using System;
89
using System.Collections.Generic;
910
using System.IO;
11+
using System.Linq;
1012
using Xunit;
1113

14+
using static DocumentFormat.OpenXml.Tests.TestAssets;
15+
1216
namespace DocumentFormat.OpenXml.Tests
1317
{
1418
public class DocumentOpenTests
@@ -35,5 +39,114 @@ public void ThrowIfFileCannotBeFound()
3539
Assert.Throws<FileNotFoundException>(() => PresentationDocument.Open(NonExistantFile, true, new OpenSettings()));
3640
Assert.Throws<FileNotFoundException>(() => PresentationDocument.Open(NonExistantFile, false, new OpenSettings()));
3741
}
42+
43+
[Theory]
44+
[InlineData(FileAccess.Read)]
45+
[InlineData(FileAccess.ReadWrite)]
46+
public void RewriteMalformedUriLong(FileAccess access)
47+
{
48+
// Arrange
49+
const string ExpectedMalformedUri = "mailto:[email protected];%[email protected];%[email protected];%[email protected];%[email protected];%[email protected]?subject=Unsubscribe%20Request&body=Please%20unsubscribe%20me%20from%20all%20future%20communications";
50+
const string Id = "rId1";
51+
52+
using var file = OpenFile(TestFiles.Malformed_uri_long_xlsx, access);
53+
using var stream = file.Open();
54+
using var doc = SpreadsheetDocument.Open(stream, isEditable: file.IsEditable);
55+
56+
// Act
57+
var worksheetPart = doc.WorkbookPart.WorksheetParts.Single();
58+
var hyperlinkRelationship = worksheetPart.HyperlinkRelationships.Single();
59+
var worksheet = Assert.IsType<Worksheet>(worksheetPart.RootElement);
60+
var hyperlink = Assert.Single(worksheet.Descendants<Hyperlink>());
61+
62+
// Assert
63+
Assert.Equal(Id, hyperlinkRelationship.Id);
64+
Assert.Equal(Id, hyperlink.Id);
65+
Assert.Equal(ExpectedMalformedUri, hyperlinkRelationship.Uri.ToString());
66+
}
67+
68+
[Theory]
69+
[InlineData(FileAccess.Read)]
70+
[InlineData(FileAccess.ReadWrite)]
71+
public void RewriteMalformedUri(FileAccess access)
72+
{
73+
// Arrange
74+
const string Id = "rId1";
75+
76+
using var file = OpenFile(TestFiles.Malformed_uri_xlsx, access);
77+
using var stream = file.Open();
78+
using var doc = SpreadsheetDocument.Open(stream, isEditable: file.IsEditable);
79+
80+
// Act
81+
var worksheetPart = doc.WorkbookPart.WorksheetParts.Single();
82+
var hyperlinkRelationship = worksheetPart.HyperlinkRelationships.Single();
83+
var worksheet = Assert.IsType<Worksheet>(worksheetPart.RootElement);
84+
var hyperlink = Assert.Single(worksheet.Descendants<Hyperlink>());
85+
86+
// Assert
87+
Assert.Equal(Id, hyperlink.Id);
88+
Assert.Equal(Id, hyperlinkRelationship.Id);
89+
Assert.Equal("mailto:one@", hyperlinkRelationship.Uri.ToString());
90+
}
91+
92+
[Fact]
93+
public void NonSeekableRewriteMalformedUri()
94+
{
95+
// Arrange
96+
using var stream = GetStream(TestFiles.Malformed_uri_xlsx);
97+
98+
var exception = Assert.Throws<OpenXmlPackageException>(() =>
99+
{
100+
using var doc = SpreadsheetDocument.Open(new NonSeekableStream(stream), isEditable: false);
101+
102+
// Act/Assert
103+
var worksheetPart = doc.WorkbookPart.WorksheetParts.Single();
104+
var link = worksheetPart.HyperlinkRelationships.Single();
105+
});
106+
107+
#if NETFRAMEWORK
108+
Assert.IsType<ArgumentException>(exception.InnerException);
109+
#else
110+
Assert.IsType<UriFormatException>(exception.InnerException);
111+
#endif
112+
}
113+
114+
[Fact]
115+
public void NonSeekableRewriteMalformedUriCompatMode()
116+
{
117+
// Arrange
118+
using var stream = GetStream(TestFiles.Malformed_uri_xlsx);
119+
120+
// Act/Assert
121+
var exception = Assert.Throws<OpenXmlPackageException>(() => SpreadsheetDocument.Open(new NonSeekableStream(stream), isEditable: false, new OpenSettings { CompatibilityLevel = CompatibilityLevel.Version_2_20 }));
122+
123+
#if NETFRAMEWORK
124+
Assert.IsType<ArgumentException>(exception.InnerException);
125+
#else
126+
Assert.IsType<UriFormatException>(exception.InnerException);
127+
#endif
128+
}
129+
130+
private sealed class NonSeekableStream : DelegatingStream
131+
{
132+
public NonSeekableStream(Stream innerStream)
133+
: base(innerStream)
134+
{
135+
}
136+
137+
public override bool CanSeek => false;
138+
139+
public override long Seek(long offset, SeekOrigin origin) => throw new NotSupportedException();
140+
141+
public override long Position
142+
{
143+
get => throw new NotSupportedException();
144+
set => throw new NotSupportedException();
145+
}
146+
147+
public override long Length => throw new NotSupportedException();
148+
149+
public override void SetLength(long value) => throw new NotSupportedException();
150+
}
38151
}
39152
}

0 commit comments

Comments
 (0)