diff --git a/src/DocumentFormat.OpenXml.Framework/Packaging/OpenXmlPart.cs b/src/DocumentFormat.OpenXml.Framework/Packaging/OpenXmlPart.cs index 873e15107..7ae133f85 100644 --- a/src/DocumentFormat.OpenXml.Framework/Packaging/OpenXmlPart.cs +++ b/src/DocumentFormat.OpenXml.Framework/Packaging/OpenXmlPart.cs @@ -448,6 +448,19 @@ internal virtual bool IsInVersion(FileFormatVersions version) return true; } + internal override bool IsEmptyPart() + { + if (!Uri.ToString().EndsWith(".xml", System.StringComparison.InvariantCultureIgnoreCase)) + { + return false; + } + + using (Stream stream = GetStream()) + { + return stream.Length == 0; + } + } + #endregion #region internal methods diff --git a/src/DocumentFormat.OpenXml.Framework/Packaging/OpenXmlPartContainer.cs b/src/DocumentFormat.OpenXml.Framework/Packaging/OpenXmlPartContainer.cs index 442154644..bd9d8e78c 100644 --- a/src/DocumentFormat.OpenXml.Framework/Packaging/OpenXmlPartContainer.cs +++ b/src/DocumentFormat.OpenXml.Framework/Packaging/OpenXmlPartContainer.cs @@ -1657,6 +1657,11 @@ internal OpenXmlPart CreateOpenXmlPart(string relationshipType) // find all reachable parts from the package root, the dictionary also used for cycle reference defense internal abstract void FindAllReachableParts(IDictionary reachableParts); + internal virtual bool IsEmptyPart() + { + return false; + } + #endregion // Checks if the target part is in the same OpenXmlPackage as this part. diff --git a/src/DocumentFormat.OpenXml.Framework/Validation/DocumentValidator.cs b/src/DocumentFormat.OpenXml.Framework/Validation/DocumentValidator.cs index df3ea4e46..c66225b5b 100644 --- a/src/DocumentFormat.OpenXml.Framework/Validation/DocumentValidator.cs +++ b/src/DocumentFormat.OpenXml.Framework/Validation/DocumentValidator.cs @@ -106,6 +106,19 @@ private void ValidatePart(OpenXmlPart part, ValidationContext context) { Validate(context); } + else if (part.IsEmptyPart()) + { + context.AddError(new ValidationErrorInfo + { + ErrorType = ValidationErrorType.Schema, + Id = "Sch_MissingPartRootElement", + Part = part, + Description = SR.Format(ValidationResources.Sch_MissingPartRootElement, part.Uri), + }); + + // The part's root element is empty, so no more errors in this part. Release the DOM to GC memory + part.UnloadRootElement(); + } if (!partRootElementLoaded && context.Errors.Count == lastErrorCount) { diff --git a/src/DocumentFormat.OpenXml.Framework/Validation/ValidationResources.Designer.cs b/src/DocumentFormat.OpenXml.Framework/Validation/ValidationResources.Designer.cs index 0f07d0a74..2b484e862 100644 --- a/src/DocumentFormat.OpenXml.Framework/Validation/ValidationResources.Designer.cs +++ b/src/DocumentFormat.OpenXml.Framework/Validation/ValidationResources.Designer.cs @@ -438,6 +438,15 @@ internal static string Sch_MinLengthConstraintFailed { } } + /// + /// Looks up a localized string similar to The '{0}' part is missing its root element.. + /// + internal static string Sch_MissingPartRootElement { + get { + return ResourceManager.GetString("Sch_MissingPartRootElement", resourceCulture); + } + } + /// /// Looks up a localized string similar to The required attribute '{0}' is missing.. /// diff --git a/src/DocumentFormat.OpenXml.Framework/Validation/ValidationResources.resx b/src/DocumentFormat.OpenXml.Framework/Validation/ValidationResources.resx index ed8489361..836e53611 100644 --- a/src/DocumentFormat.OpenXml.Framework/Validation/ValidationResources.resx +++ b/src/DocumentFormat.OpenXml.Framework/Validation/ValidationResources.resx @@ -342,4 +342,7 @@ Cell contents have invalid value '{0}' for type '{1}'. + + The '{0}' part is missing its root element. + \ No newline at end of file diff --git a/test/DocumentFormat.OpenXml.Tests/DocxTests01.cs b/test/DocumentFormat.OpenXml.Tests/DocxTests01.cs index f69761522..48321869f 100644 --- a/test/DocumentFormat.OpenXml.Tests/DocxTests01.cs +++ b/test/DocumentFormat.OpenXml.Tests/DocxTests01.cs @@ -1,6 +1,7 @@ // 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.CustomProperties; using DocumentFormat.OpenXml.Packaging; using DocumentFormat.OpenXml.Validation; using System; @@ -136,8 +137,8 @@ public void W051_AddNewPart_ToOpenXmlPackage() using (var stream = GetStream(TestFiles.Hyperlink, true)) using (var doc = WordprocessingDocument.Open(stream, true)) { - var pkg = (OpenXmlPackage)doc; - var wpcp = pkg.AddNewPart("application/xml", "rid1232131"); + var footer = doc.MainDocumentPart.AddNewPart(); + footer.Footer = new W.Footer(); var v = new OpenXmlValidator(FileFormatVersions.Office2013); var errs = v.Validate(doc, TestContext.Current.CancellationToken); @@ -156,12 +157,24 @@ public void W050_DeleteAdd_CoreExtendedProperties() doc.DeletePart(corePart); doc.DeletePart(appPart); - doc.AddCoreFilePropertiesPart(); - doc.AddExtendedFilePropertiesPart(); - doc.AddCustomFilePropertiesPart(); + var cfpp = doc.AddCoreFilePropertiesPart(); + + string xml = "hello"; + byte[] corePropsByteArray = System.Text.Encoding.UTF8.GetBytes(xml); + using (var stream1 = new MemoryStream(corePropsByteArray)) + { + cfpp.FeedData(stream1); + } + + var efpp = doc.AddExtendedFilePropertiesPart(); + efpp.Properties = new ExtendedProperties.Properties(); + + var cusfpp = doc.AddCustomFilePropertiesPart(); + cusfpp.Properties = new CustomProperties.Properties(); + doc.AddDigitalSignatureOriginPart(); - doc.AddExtendedPart("relType", "contentType/xml", ".xml"); + doc.AddExtendedPart("relType", "contentType/xml", ".xml"); var tnPart = doc.AddThumbnailPart(ThumbnailPartType.Jpeg); doc.DeletePart(tnPart); tnPart = doc.AddThumbnailPart("image/jpg"); @@ -180,6 +193,7 @@ public void W049_AddNewPart_ToPackage() using (var doc = WordprocessingDocument.Open(stream, true)) { var wpcp = doc.AddNewPart("application/xml", "rid1232131"); + wpcp.CustomUI = new Office.CustomUI.CustomUI(); var v = new OpenXmlValidator(FileFormatVersions.Office2013); var errs = v.Validate(doc, TestContext.Current.CancellationToken); @@ -207,6 +221,7 @@ public void W047_AddNewPart_ToPackage() using (var doc = WordprocessingDocument.Open(stream, true)) { var wpcp = doc.AddNewPart("rid123123"); + wpcp.CustomUI = new Office.CustomUI.CustomUI(); var v = new OpenXmlValidator(FileFormatVersions.Office2013); var errs = v.Validate(doc, TestContext.Current.CancellationToken); @@ -239,6 +254,7 @@ public void W045_AddNewPart_ToPart() using (var doc = WordprocessingDocument.Open(stream, true)) { var wpcp = doc.MainDocumentPart.AddNewPart("application/vnd.openxmlformats-officedocument.wordprocessingml.comments+xml", "rid1232131"); + wpcp.Comments = new W.Comments(); var v = new OpenXmlValidator(FileFormatVersions.Office2013); var errs = v.Validate(doc, TestContext.Current.CancellationToken); @@ -266,6 +282,7 @@ public void W043_AddNewPart() using (var doc = WordprocessingDocument.Open(stream, true)) { var wpcp = doc.MainDocumentPart.AddNewPart("rid123123"); + wpcp.Comments = new W.Comments(); var v = new OpenXmlValidator(FileFormatVersions.Office2013); var errs = v.Validate(doc, TestContext.Current.CancellationToken); @@ -280,6 +297,7 @@ public void W042_AddNewPart() using (var doc = WordprocessingDocument.Open(stream, true)) { var wpcp = doc.MainDocumentPart.AddNewPart(); + wpcp.Comments = new W.Comments(); var v = new OpenXmlValidator(FileFormatVersions.Office2013); var errs = v.Validate(doc, TestContext.Current.CancellationToken); diff --git a/test/DocumentFormat.OpenXml.Tests/PptxTests01.cs b/test/DocumentFormat.OpenXml.Tests/PptxTests01.cs index fb888cde3..3c516554c 100644 --- a/test/DocumentFormat.OpenXml.Tests/PptxTests01.cs +++ b/test/DocumentFormat.OpenXml.Tests/PptxTests01.cs @@ -389,6 +389,7 @@ public void P002_Pptx_DeleteAdd_CoreExtendedProperties() var corePart = doc.CoreFilePropertiesPart; var appPart = doc.ExtendedFilePropertiesPart; var custFilePropsPart = doc.CustomFilePropertiesPart; + var thumbNailPart = doc.ThumbnailPart; doc.DeletePart(corePart); @@ -399,9 +400,96 @@ public void P002_Pptx_DeleteAdd_CoreExtendedProperties() doc.DeletePart(thumbNailPart); } - doc.AddCoreFilePropertiesPart(); - doc.AddExtendedFilePropertiesPart(); - doc.AddCustomFilePropertiesPart(); + var coreFPP = doc.AddCoreFilePropertiesPart(); + var coreFPPStream = coreFPP.GetStream(); + using (var writer = new System.Xml.XmlTextWriter(coreFPPStream, System.Text.Encoding.UTF8)) + { + writer.WriteRaw(""" + + + + Joey Daccord + Joey Daccord + 2 + 2025-06-12T19:21:34Z + 2025-06-12T21:23:11Z + + """); + writer.Flush(); + } + + var appFPP = doc.AddExtendedFilePropertiesPart(); + var appFPPStream = appFPP.GetStream(); + using (var writer = new System.Xml.XmlTextWriter(appFPPStream, System.Text.Encoding.UTF8)) + { + writer.WriteRaw(""" + + + 12 + 0 + Microsoft Office PowerPoint + Widescreen + 0 + 1 + 0 + 0 + 0 + false + + + + Fonts Used + + + 3 + + + Theme + + + 1 + + + Slide Titles + + + 1 + + + + + + Aptos + Aptos Display + Arial + Office Theme + PowerPoint Presentation + + + + false + false + false + 16.0000 + + """); + } + + var custFPP = doc.AddCustomFilePropertiesPart(); + var custFPPStream = custFPP.GetStream(); + using (var writer = new System.Xml.XmlTextWriter(custFPPStream, System.Text.Encoding.UTF8)) + { + writer.WriteRaw(""" + + + + foobar + + + """); + writer.Flush(); + } + doc.AddDigitalSignatureOriginPart(); doc.AddExtendedPart("relType", "contentType/xml", ".xml"); diff --git a/test/DocumentFormat.OpenXml.Tests/XlsxTests01.cs b/test/DocumentFormat.OpenXml.Tests/XlsxTests01.cs index 21f91112b..53c50fa1f 100644 --- a/test/DocumentFormat.OpenXml.Tests/XlsxTests01.cs +++ b/test/DocumentFormat.OpenXml.Tests/XlsxTests01.cs @@ -67,9 +67,78 @@ public void X006_Xlsx_DeleteAdd_CoreExtendedProperties() var appPart = doc.ExtendedFilePropertiesPart; doc.DeletePart(corePart); doc.DeletePart(appPart); - doc.AddCoreFilePropertiesPart(); - doc.AddExtendedFilePropertiesPart(); - doc.AddCustomFilePropertiesPart(); + var cFPP = doc.AddCoreFilePropertiesPart(); + var cFPPStream = cFPP.GetStream(); + + using (var writer = new System.Xml.XmlTextWriter(cFPPStream, System.Text.Encoding.UTF8)) + { + writer.WriteRaw(""" + + + Shohei Ohtani + Shohei Ohtani + 2015-06-05T18:17:20Z + 2025-06-13T17:11:50Z + + """); + + writer.Flush(); + } + + var eFPP = doc.AddExtendedFilePropertiesPart(); + var eFPPStream = eFPP.GetStream(); + + using (var writer = new System.Xml.XmlTextWriter(eFPPStream, System.Text.Encoding.UTF8)) + { + writer.WriteRaw(""" + + + Microsoft Excel + 0 + false + + + + Worksheets + + + 1 + + + + + + Sheet1 + + + + false + false + false + 16.0300 + + """); + + writer.Flush(); + } + + var custFPP = doc.AddCustomFilePropertiesPart(); + var custFPPStream = custFPP.GetStream(); + + using (var writer = new System.Xml.XmlTextWriter(custFPPStream, System.Text.Encoding.UTF8)) + { + writer.WriteRaw(""" + + + + tacocat + + + """); + + writer.Flush(); + } + doc.AddDigitalSignatureOriginPart(); doc.AddExtendedPart("relType", "contentType/xml", ".xml"); var tnPart = doc.AddThumbnailPart(ThumbnailPartType.Jpeg); diff --git a/test/DocumentFormat.OpenXml.Tests/ofapiTest/OpenXmlValidatorTest.cs b/test/DocumentFormat.OpenXml.Tests/ofapiTest/OpenXmlValidatorTest.cs index 4fdeb0daa..4ee2f6a20 100644 --- a/test/DocumentFormat.OpenXml.Tests/ofapiTest/OpenXmlValidatorTest.cs +++ b/test/DocumentFormat.OpenXml.Tests/ofapiTest/OpenXmlValidatorTest.cs @@ -3832,5 +3832,23 @@ public void VersionMismatchPartValidatingTest() Assert.Throws(() => O14Validator.Validate(wordTestDocument.MainDocumentPart, TestContext.Current.CancellationToken)); } } + + [Fact] + public void EmptyPartRootElementValidatingTest() + { + using (Stream stream = new MemoryStream()) + using (WordprocessingDocument document = WordprocessingDocument.Create(stream, WordprocessingDocumentType.Document)) + { + document.AddMainDocumentPart(); + + IEnumerable errors = O14Validator.Validate(document, TestContext.Current.CancellationToken); + ValidationErrorInfo info = errors.FirstOrDefault(); + + Assert.Single(errors); + Assert.Equal("Sch_MissingPartRootElement", info.Id); + Assert.Equal("The '/word/document.xml' part is missing its root element.", info.Description); + Assert.Equal(ValidationErrorType.Schema, info.ErrorType); + } + } } }