Skip to content

Commit df6e922

Browse files
committed
Merge upstream changes from dotnet/Open-XML-SDK
Merging latest changes from upstream: - add error for encrypted files (dotnet#1969)
2 parents 28bc2d7 + 9c7ea43 commit df6e922

File tree

10 files changed

+204
-2
lines changed

10 files changed

+204
-2
lines changed

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

Lines changed: 6 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -56,7 +56,12 @@ public StreamPackageFeature(Stream stream, PackageOpenMode openMode, bool isOwne
5656
}
5757
catch when (isOwned)
5858
{
59-
// Ensure that the stream if created is disposed before leaving the constructor so we don't hold onto it
59+
if (_stream is not null && OpenXmlPackage.IsEncryptedOfficeFile(_stream))
60+
{
61+
_stream.Dispose();
62+
throw new OpenXmlPackageException(ExceptionMessages.EncryptedPackageNotSupported);
63+
}
64+
6065
_stream?.Dispose();
6166
throw;
6267
}

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

Lines changed: 92 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -612,5 +612,97 @@ public void Save()
612612

613613
/// <inheritdoc/>
614614
public override IFeatureCollection Features => _features ??= new PackageFeatureCollection(this);
615+
616+
/// <summary>
617+
/// Determines whether the provided stream represents an encrypted Office Open XML file.
618+
/// </summary>
619+
/// <param name="inputStream">The <see cref="Stream"/> to check. The stream must be seekable and not null.</param>
620+
/// <returns>
621+
/// <c>true</c> if the stream is an encrypted Office file (either OLE Compound File or contains an encrypted package part); otherwise, <c>false</c>.
622+
/// </returns>
623+
/// <exception cref="ArgumentNullException">Thrown if <paramref name="inputStream"/> is null.</exception>
624+
/// <exception cref="ArgumentException">Thrown if <paramref name="inputStream"/> is not seekable.</exception>
625+
/// <remarks>
626+
/// This method checks for the OLE Compound File signature at the start of the stream, which is used for encrypted Office files.
627+
/// If not found, it attempts to open the stream as an OPC package and checks for the presence of an encrypted package part.
628+
/// The stream position is restored after the check.
629+
/// </remarks>
630+
public static bool IsEncryptedOfficeFile(Stream inputStream)
631+
{
632+
if (inputStream is null)
633+
{
634+
throw new ArgumentNullException(nameof(inputStream));
635+
}
636+
637+
if (!inputStream.CanSeek)
638+
{
639+
throw new ArgumentException("Stream must be seekable.");
640+
}
641+
642+
long originalPosition = inputStream.Position;
643+
644+
try
645+
{
646+
byte[] header = new byte[8];
647+
inputStream.Seek(0, SeekOrigin.Begin);
648+
int read = inputStream.Read(header, 0, header.Length);
649+
inputStream.Seek(originalPosition, SeekOrigin.Begin);
650+
651+
// OLE Compound File signature for encrypted Office files
652+
if (read == 8 && header.SequenceEqual(new byte[] { 0xD0, 0xCF, 0x11, 0xE0, 0xA1, 0xB1, 0x1A, 0xE1 }))
653+
{
654+
return true;
655+
}
656+
657+
// If not OLE, try to open as package and check for encrypted part
658+
try
659+
{
660+
using (var package = System.IO.Packaging.Package.Open(inputStream, FileMode.Open, FileAccess.Read))
661+
{
662+
foreach (var part in package.GetParts())
663+
{
664+
if (part.ContentType.Equals("application/vnd.openxmlformats-officedocument.encrypted-package", StringComparison.OrdinalIgnoreCase))
665+
{
666+
return true;
667+
}
668+
}
669+
}
670+
}
671+
catch
672+
{
673+
return false;
674+
}
675+
676+
return false;
677+
}
678+
finally
679+
{
680+
inputStream.Seek(originalPosition, SeekOrigin.Begin);
681+
}
682+
}
683+
684+
/// <summary>
685+
/// Determines whether the file at the specified path is an encrypted Office Open XML file.
686+
/// </summary>
687+
/// <param name="filePath">The path to the file to check. Must not be null.</param>
688+
/// <returns>
689+
/// <c>true</c> if the file is an encrypted Office file (either OLE Compound File or contains an encrypted package part); otherwise, <c>false</c>.
690+
/// </returns>
691+
/// <exception cref="ArgumentNullException">Thrown if <paramref name="filePath"/> is null.</exception>
692+
/// <remarks>
693+
/// This method opens the file at the specified path and checks its contents using <see cref="IsEncryptedOfficeFile(Stream)"/>.
694+
/// </remarks>
695+
public static bool IsEncryptedOfficeFile(string filePath)
696+
{
697+
if (filePath is null)
698+
{
699+
throw new ArgumentNullException(nameof(filePath));
700+
}
701+
702+
using (FileStream fileStream = File.Open(filePath, FileMode.Open, FileAccess.Read, FileShare.Read))
703+
{
704+
return IsEncryptedOfficeFile(fileStream);
705+
}
706+
}
615707
}
616708
}

src/DocumentFormat.OpenXml.Framework/PublicAPI/PublicAPI.Shipped.txt

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1010,3 +1010,5 @@ DocumentFormat.OpenXml.OpenXmlPartWriterSettings.OpenXmlPartWriterSettings() ->
10101010
DocumentFormat.OpenXml.OpenXmlPartWriter.OpenXmlPartWriter(DocumentFormat.OpenXml.Packaging.OpenXmlPart! openXmlPart, DocumentFormat.OpenXml.OpenXmlPartWriterSettings! settings) -> void
10111011
DocumentFormat.OpenXml.OpenXmlPartWriter.OpenXmlPartWriter(System.IO.Stream! partStream, DocumentFormat.OpenXml.OpenXmlPartWriterSettings! settings) -> void
10121012
virtual DocumentFormat.OpenXml.OpenXmlCompositeElement.IsValidChild(DocumentFormat.OpenXml.OpenXmlElement! element) -> bool
1013+
static DocumentFormat.OpenXml.Packaging.OpenXmlPackage.IsEncryptedOfficeFile(System.IO.Stream! inputStream) -> bool
1014+
static DocumentFormat.OpenXml.Packaging.OpenXmlPackage.IsEncryptedOfficeFile(string! filePath) -> bool

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

Lines changed: 10 additions & 1 deletion
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: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -414,4 +414,7 @@
414414
<data name="FirstOrDefaultMaxOne" xml:space="preserve">
415415
<value>The enumerable contained more than a single element when only zero or one are allowed.</value>
416416
</data>
417+
<data name="EncryptedPackageNotSupported" xml:space="preserve">
418+
<value>Encrypted packages are not supported.</value>
419+
</data>
417420
</root>

test/DocumentFormat.OpenXml.Framework.Tests/Features/StreamPackageFeatureTests.cs

Lines changed: 13 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -11,6 +11,8 @@
1111
using System.Linq;
1212
using Xunit;
1313

14+
using static DocumentFormat.OpenXml.Tests.TestAssets;
15+
1416
namespace DocumentFormat.OpenXml.Features.Tests;
1517

1618
public class StreamPackageFeatureTests
@@ -431,6 +433,17 @@ protected override void Dispose(bool disposing)
431433
}
432434
}
433435

436+
[Fact]
437+
public void ThrowsForEncryptedOfficeFile()
438+
{
439+
using (Stream stream = GetStream(TestFiles.Encrypted_pptx, false))
440+
{
441+
// Act & Assert
442+
var ex = Assert.Throws<OpenXmlPackageException>(() => new StreamPackageFeature(stream, PackageOpenMode.Read, isOwned: true));
443+
Assert.Equal(ExceptionMessages.EncryptedPackageNotSupported, ex.Message);
444+
}
445+
}
446+
434447
private static readonly PartInfo Part1 = new(new("/part1", UriKind.Relative), "type1/content");
435448
private static readonly PartInfo Part2 = new(new("/part2", UriKind.Relative), "type2/content");
436449
private static readonly PartInfo PartRels = new(new("/_rels/.rels", UriKind.Relative), "application/vnd.openxmlformats-package.relationships+xml");

test/DocumentFormat.OpenXml.Packaging.Tests/OpenXmlPackageTests.cs

Lines changed: 56 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -342,5 +342,61 @@ public void SucceedWithMissingCalcChainPart()
342342

343343
Assert.NotNull(spd);
344344
}
345+
346+
[Fact]
347+
public void IsEncryptedOfficeFile_ReturnsTrue_ForEncryptedFile()
348+
{
349+
using (Stream stream = GetStream(TestFiles.Encrypted_pptx, false))
350+
{
351+
Assert.True(OpenXmlPackage.IsEncryptedOfficeFile(stream));
352+
}
353+
}
354+
355+
[Fact]
356+
public void IsEncryptedOfficeFile_ReturnsFalse_ForUnencryptedFile()
357+
{
358+
using (Stream stream = GetStream(TestFiles.Presentation, false))
359+
{
360+
Assert.False(OpenXmlPackage.IsEncryptedOfficeFile(stream));
361+
}
362+
}
363+
364+
[Fact]
365+
public void IsEncryptedOfficeFile_ThrowsArgumentNullException_ForNullStream()
366+
{
367+
Assert.Throws<ArgumentNullException>(() => OpenXmlPackage.IsEncryptedOfficeFile((Stream)null!));
368+
}
369+
370+
[Fact]
371+
public void IsEncryptedOfficeFile_ThrowsArgumentException_ForUnseekableStream()
372+
{
373+
var unseekable = new UnseekableStream();
374+
Assert.Throws<ArgumentException>(() => OpenXmlPackage.IsEncryptedOfficeFile(unseekable));
375+
}
376+
377+
private class UnseekableStream : MemoryStream
378+
{
379+
public override bool CanSeek => false;
380+
}
381+
382+
[Fact]
383+
public void IsEncryptedOfficeFile_ReturnsTrue_ForEncryptedFilePath()
384+
{
385+
string filePath = GetTestFilePath(TestFiles.Encrypted_pptx);
386+
Assert.True(OpenXmlPackage.IsEncryptedOfficeFile(filePath));
387+
388+
// Clean up the test file path
389+
File.Delete(filePath);
390+
}
391+
392+
[Fact]
393+
public void IsEncryptedOfficeFile_ReturnsFalse_ForUnencryptedFile_FromString()
394+
{
395+
string filePath = GetTestFilePath(TestFiles.Presentation);
396+
Assert.False(OpenXmlPackage.IsEncryptedOfficeFile(filePath));
397+
398+
// Clean up the test file path
399+
File.Delete(filePath);
400+
}
345401
}
346402
}

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

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -127,6 +127,8 @@ public static class Templates
127127
public const string Of16_09_unknownelement_docx = "TestFiles.Of16-09-UnknownElement.docx";
128128

129129
public const string Of16_10_symex_docx = "TestFiles.Of16-10-SymEx.docx";
130+
131+
public const string Encrypted_pptx = "TestFiles.encrypted_pptx.pptx";
130132
}
131133
}
132134
}

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

Lines changed: 20 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -71,6 +71,26 @@ public static Stream GetStream(string name, bool isEditable)
7171
return isEditable ? stream.AsMemoryStream() : stream;
7272
}
7373

74+
/// <summary>
75+
/// Extracts an embedded test resource to a temporary file and returns its file path.
76+
/// </summary>
77+
/// <param name="resourceName">The name of the embedded resource to extract.</param>
78+
/// <returns>The full path to the temporary file containing the resource data.</returns>
79+
/// <remarks>
80+
/// The caller is responsible for deleting the temporary file after use.
81+
/// </remarks>
82+
public static string GetTestFilePath(string resourceName)
83+
{
84+
string tempPath = Path.Combine(Path.GetTempPath(), Guid.NewGuid() + Path.GetExtension(resourceName));
85+
86+
using (Stream stream = GetStream(resourceName, false))
87+
using (FileStream fileStream = File.Create(tempPath))
88+
{
89+
stream.CopyTo(fileStream);
90+
return tempPath;
91+
}
92+
}
93+
7494
private static Stream AsMemoryStream(this Stream stream)
7595
{
7696
if (stream is MemoryStream ms)
Binary file not shown.

0 commit comments

Comments
 (0)