-
Notifications
You must be signed in to change notification settings - Fork 598
Expand file tree
/
Copy pathStreamPackageFeature.cs
More file actions
285 lines (241 loc) · 8.72 KB
/
Copy pathStreamPackageFeature.cs
File metadata and controls
285 lines (241 loc) · 8.72 KB
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
// Copyright (c) Microsoft. All rights reserved.
// Licensed under the MIT license. See LICENSE file in the project root for full license information.
using DocumentFormat.OpenXml.Builder;
using DocumentFormat.OpenXml.Framework;
using DocumentFormat.OpenXml.Packaging;
using System;
using System.Diagnostics.CodeAnalysis;
using System.IO;
#if !NETFRAMEWORK
using System.IO.Compression;
#endif
using System.IO.Packaging;
namespace DocumentFormat.OpenXml.Features;
internal class StreamPackageFeature : PackageFeatureBase, IDisposable, IPackageStreamFeature
{
private Package _package;
private Stream _stream;
private bool _disposedValue;
private bool _isOwned;
public StreamPackageFeature(Stream stream, PackageOpenMode openMode, bool isOwned = false)
{
if (stream is null)
{
throw new ArgumentNullException(nameof(stream));
}
if (!stream.CanRead)
{
throw new OpenXmlPackageException(ExceptionMessages.StreamAccessModeShouldRead);
}
if (openMode != PackageOpenMode.Read && !stream.CanWrite)
{
throw new OpenXmlPackageException(ExceptionMessages.StreamAccessModeShouldBeWrite);
}
// Ensure the stream we're operating on is readonly if that is the requested mode
_stream = openMode == PackageOpenMode.Read && stream.CanWrite ? new ReadOnlyStream(stream) : stream;
var initialMode = openMode switch
{
PackageOpenMode.Create => FileMode.Create,
PackageOpenMode.Read => FileMode.Open,
PackageOpenMode.ReadWrite => FileMode.OpenOrCreate,
_ => throw new NotImplementedException(),
};
// Only the initial should create, after that, it should be open
Mode = initialMode == FileMode.Create ? Mode = FileMode.Open : initialMode;
Access = openMode == PackageOpenMode.Read ? FileAccess.Read : FileAccess.ReadWrite;
try
{
InitializePackage(initialMode, Access);
}
catch when (isOwned)
{
if (_stream is not null && OpenXmlPackage.IsEncryptedOfficeFile(_stream))
{
_stream.Dispose();
throw new OpenXmlPackageException(ExceptionMessages.EncryptedPackageNotSupported);
}
_stream?.Dispose();
throw;
}
_isOwned = isOwned;
}
protected FileAccess Access { get; }
protected FileMode Mode { get; }
public Stream Stream
{
get => _stream;
set
{
if (SetStream(value))
{
// Once we set a new stream, it will be owned by the package
_isOwned = true;
Reload();
}
}
}
[MemberNotNull(nameof(_stream))]
private bool SetStream(Stream stream)
{
if (ReferenceEquals(_stream, stream))
{
return false;
}
DisposeStreamIfOwned();
_stream = stream;
return true;
}
protected void DisposeStreamIfOwned()
{
if (_isOwned && _stream is not null)
{
_stream.Dispose();
_stream = null!;
}
}
protected virtual Stream GetStream(FileMode mode, FileAccess access) => Stream;
[MemberNotNull(nameof(_package))]
private void InitializePackage(FileMode? mode = default, FileAccess? access = default)
{
mode ??= Mode;
access ??= Access;
_package?.Close();
if (_stream is null || Mode != mode || Access != access)
{
SetStream(GetStream(mode.Value, access.Value));
}
try
{
_stream = RepairRelsContentType(_stream);
_package = Package.Open(_stream, mode.Value, access.Value);
}
catch (ArgumentException ex)
{
throw new OpenXmlPackageException(ExceptionMessages.FailedToOpenPackage, ex);
}
}
protected override Package Package => _package;
public override PackageCapabilities Capabilities => base.Capabilities | PackageCapabilities.Reload;
public override void Reload(FileMode? mode = null, FileAccess? access = null)
{
InitializePackage(mode, access);
UpdateCachedItems();
}
protected virtual void Dispose(bool disposing)
{
if (!_disposedValue)
{
if (disposing)
{
_package.Close();
if (_isOwned)
{
_stream.Dispose();
}
}
_disposedValue = true;
}
}
public void Dispose()
{
Dispose(disposing: true);
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\"";
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
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);
features.Set<IPackageStreamFeature>(this);
features.GetRequired<IDisposableFeature>().Register(this);
}
}