using System.Text;
using OdfKit.Compliance;
using OdfKit.Core;
using OdfKit.Chart;
using OdfKit.Image;
using OdfKit.Database;
using OdfKit.Styles;
using System.Xml.Linq;
using Xunit;
namespace OdfKit.Tests;
[Trait(TestCategories.Kind, TestCategories.Compliance)]
public class OdfConformanceGapTests
{
[Theory]
[InlineData(OdfVersion.Odf12)]
[InlineData(OdfVersion.Odf13)]
[InlineData(OdfVersion.Odf14)]
public void ForeignElementXmlIdsStillRequireNcNames(OdfVersion version)
{
string xml = $"Text";
Assert.Contains(ValidateContent(xml, OdfDocumentKind.Text, version).Issues,
issue => issue.RuleId == "ODF3105" && issue.PackagePath == "content.xml");
Assert.DoesNotContain(ValidateContent(xml.Replace("bad:id", " valid ", StringComparison.Ordinal), OdfDocumentKind.Text, version).Issues,
issue => issue.RuleId == "ODF3105");
}
[Fact]
public void DatabaseTableCommandsUseIsolatedSdkMetadataAndMigrateOnUpdate()
{
using DatabaseDocument database = DatabaseDocument.Create();
database.AddTable("Customers", "SELECT * FROM Customers");
var table = Assert.Single(database.GetTables());
Assert.Equal("SELECT * FROM Customers", table.Command);
using var stream = new MemoryStream();
database.SaveToStream(stream);
stream.Position = 0;
using OdfPackage package = OdfPackage.Open(stream);
OdfValidationReport extended = OdfPackageValidator.Validate(package, OdfComplianceProfiles.OasisOdf14Extended);
Assert.True(extended.IsValid, extended.ToJson());
OdfValidationReport strict = OdfPackageValidator.Validate(package, OdfComplianceProfiles.OasisOdf14Strict);
Assert.Contains(strict.Issues, issue => issue.RuleId == "ODF3101");
var node = database.AddTable("Legacy");
node.SetAttribute("command", OdfNamespaces.Database, "old", "db");
Assert.Equal("old", database.FindTable("Legacy")?.Command);
Assert.True(database.UpdateTable("Legacy", "new"));
Assert.Null(node.GetAttribute("command", OdfNamespaces.Database));
Assert.Equal("new", database.FindTable("Legacy")?.Command);
Assert.True(database.UpdateTable("Legacy", null));
Assert.Null(database.FindTable("Legacy")?.Command);
}
[Theory]
[InlineData("")]
[InlineData("AQID")]
public void FlatBinaryDataDoesNotConsumeFollowingFrameMetadata(string base64)
{
string xml = $"{base64}TitleDescription";
using var input = new MemoryStream(Encoding.UTF8.GetBytes(xml));
using FlatImageDocument image = FlatImageDocument.Load(input);
OdfImageFrameInfo frame = Assert.Single(image.GetImageFrames());
Assert.Equal("Title", frame.Title);
Assert.Equal("Description", frame.Description);
Assert.Equal(Convert.FromBase64String(base64), image.GetImageBytes());
}
[Theory]
[InlineData(OdfVersion.Odf12)]
[InlineData(OdfVersion.Odf13)]
[InlineData(OdfVersion.Odf14)]
public void QNameDatatypeUsesCurrentAndInheritedNamespaceDeclarations(OdfVersion version)
{
using OdfChartDocument chart = OdfChartDocument.Create();
using var stream = new MemoryStream();
chart.SaveToStream(stream);
stream.Position = 0;
using OdfPackage package = OdfPackage.Open(stream);
using Stream content = package.GetEntryStream("content.xml");
XDocument document = XDocument.Load(content);
XElement element = document.Descendants(XName.Get("chart", OdfNamespaces.Chart)).Single();
OdfSchemaSet schema = OdfSchemaRegistry.GetSchema(version);
element.SetAttributeValue(XName.Get("class", OdfNamespaces.Chart), "custom:bar");
Assert.False(OdfSchemaPatternValidator.ValidateElement(element, schema, "chart-chart").IsMatch);
document.Root!.SetAttributeValue(XNamespace.Xmlns + "custom", "urn:test:chart");
Assert.True(OdfSchemaPatternValidator.ValidateElement(element, schema, "chart-chart").IsMatch);
element.SetAttributeValue(XName.Get("class", OdfNamespaces.Chart), " \tcustom:bar\n ");
Assert.True(OdfSchemaPatternValidator.ValidateElement(element, schema, "chart-chart").IsMatch);
XElement plot = element.Element(XName.Get("plot-area", OdfNamespaces.Chart))!;
XElement series = new(XName.Get("series", OdfNamespaces.Chart),
new XAttribute(XName.Get("class", OdfNamespaces.Chart), "missing:line"));
plot.Add(series);
Assert.False(OdfSchemaPatternValidator.ValidateElement(element, schema, "chart-chart").IsMatch);
series.SetAttributeValue(XNamespace.Xmlns + "missing", "urn:test:series");
Assert.True(OdfSchemaPatternValidator.ValidateElement(element, schema, "chart-chart").IsMatch);
}
[Theory]
[InlineData(OdfVersion.Odf12, "formula", OdfDocumentKind.Formula)]
[InlineData(OdfVersion.Odf13, "formula", OdfDocumentKind.Formula)]
[InlineData(OdfVersion.Odf14, "formula", OdfDocumentKind.Formula)]
[InlineData(OdfVersion.Odf12, "database", OdfDocumentKind.Database)]
[InlineData(OdfVersion.Odf13, "database", OdfDocumentKind.Database)]
[InlineData(OdfVersion.Odf14, "database", OdfDocumentKind.Database)]
public void BodyChecksDoNotExemptEnvelopeFromSchemaValidation(OdfVersion version, string body, OdfDocumentKind kind)
{
string bodyContent = body == "database"
? $""
: "";
string xml = $"{bodyContent}";
OdfValidationReport valid = ValidateContent(xml, kind, version);
Assert.True(valid.IsValid, string.Join("; ", valid.Issues.Select(issue => issue.Message)));
if (body == "formula")
{
Assert.Contains(valid.Issues, issue => issue.RuleId == "ODF3103" && issue.PackagePath == "content.xml");
}
else
{
Assert.DoesNotContain(valid.Issues, issue => issue.RuleId == "ODF3103");
string missingDataSource = xml.Replace(bodyContent, "", StringComparison.Ordinal);
Assert.Contains(ValidateContent(missingDataSource, kind, version).Issues,
issue => issue.RuleId == "ODF3101" && issue.PackagePath == "content.xml");
string invalidChild = xml.Replace("", "", StringComparison.Ordinal);
Assert.Contains(ValidateContent(invalidChild, kind, version).Issues,
issue => issue.RuleId == "ODF3101" && issue.PackagePath == "content.xml");
}
string invalidAttribute = xml.Replace("", "", StringComparison.Ordinal);
OdfValidationReport invalid = ValidateContent(invalidAttribute, kind, version);
Assert.Contains(invalid.Issues, issue => issue.RuleId == "ODF3101" && issue.Severity == OdfIssueSeverity.Error);
string invalidSibling = xml.Replace("", "", StringComparison.Ordinal);
Assert.Contains(ValidateContent(invalidSibling, kind, version).Issues,
issue => issue.RuleId == "ODF3101" && issue.PackagePath == "content.xml");
}
[Fact]
public void BareMathmlExemptionIsVisibleInValidationReport()
{
OdfValidationReport report = ValidateContent("",
OdfDocumentKind.Formula, OdfVersion.Odf14);
Assert.Contains(report.Issues, issue => issue.RuleId == "ODF3103" && issue.Severity == OdfIssueSeverity.Warning);
}
[Theory]
[InlineData(OdfVersion.Odf10)]
[InlineData(OdfVersion.Odf11)]
[InlineData(OdfVersion.Odf12)]
[InlineData(OdfVersion.Odf13)]
[InlineData(OdfVersion.Odf14)]
public void NativeSchemaDoesNotRepairInvalidVersionBeforeMatching(OdfVersion version)
{
// 主文件決定 schema,另一串流的非法 version 不可先改寫成合法值才驗證。
string xml = $"";
string styles = $"";
OdfValidationReport report = ValidateContent(xml, OdfDocumentKind.Text, version, styles);
Assert.Contains(report.Issues, issue => issue.RuleId == "ODF0401" && issue.PackagePath == "styles.xml");
if (version is not (OdfVersion.Odf10 or OdfVersion.Odf11))
{
Assert.Contains(report.Issues, issue => issue.RuleId == "ODF3101" && issue.PackagePath == "styles.xml");
}
}
[Theory]
[InlineData(OdfVersion.Odf10)]
[InlineData(OdfVersion.Odf11)]
[InlineData(OdfVersion.Odf12)]
[InlineData(OdfVersion.Odf13)]
[InlineData(OdfVersion.Odf14)]
public void ProducedManifestUsesOnlyAttributesDefinedBySelectedVersion(OdfVersion version)
{
string xml = $"";
OdfValidationReport report = ValidateContent(xml, OdfDocumentKind.Text, version);
Assert.True(report.IsValid, string.Join("; ", report.Issues.Select(issue => issue.Message)));
Assert.DoesNotContain(report.Issues, issue => issue.PackagePath == "META-INF/manifest.xml");
}
[Theory]
[InlineData(OdfVersion.Odf12)]
[InlineData(OdfVersion.Odf13)]
[InlineData(OdfVersion.Odf14)]
public void XmlIdsAreUniqueWithinEachStreamAndCanRepeatAcrossEmbeddedDocuments(OdfVersion version)
{
string xml = $"AB";
OdfValidationReport valid = ValidateContent(xml, OdfDocumentKind.Text, version, embedded: xml);
Assert.True(valid.IsValid, string.Join("; ", valid.Issues.Select(issue => issue.Message)));
string duplicate = xml.Replace("xml:id=\"second\"", "xml:id=\"first\"", StringComparison.Ordinal);
Assert.Contains(ValidateContent(duplicate, OdfDocumentKind.Text, version).Issues,
issue => issue.RuleId == "ODF3104" && issue.PackagePath == "content.xml");
Assert.Contains(ValidateContent(xml, OdfDocumentKind.Text, version, embedded: duplicate).Issues,
issue => issue.RuleId == "ODF3104" && issue.PackagePath == "Object1/content.xml");
}
[Theory]
[InlineData("1.0", true)]
[InlineData("1.1", true)]
[InlineData("1.2", true)]
[InlineData("1.3", true)]
[InlineData("1.4", true)]
[InlineData("bogus", false)]
public void EmbeddedDocumentsUseTheirOwnDeclaredSchemaVersion(string embeddedVersion, bool expected)
{
string xml = $"";
string embedded = xml.Replace("office:version=\"1.4\"", $"office:version=\"{embeddedVersion}\"", StringComparison.Ordinal);
OdfValidationReport report = ValidateContent(xml, OdfDocumentKind.Text, OdfVersion.Odf14, embedded: embedded);
Assert.Equal(expected, report.IsValid);
if (!expected)
{
Assert.Contains(report.Issues, issue => issue.RuleId == "ODF0401" && issue.PackagePath == "Object1/content.xml");
}
}
[Theory]
[InlineData(OdfVersion.Odf10)]
[InlineData(OdfVersion.Odf11)]
public async Task LegacyPackageVersionSurvivesSynchronousAndAsynchronousLoadAndResave(OdfVersion version)
{
using var original = new MemoryStream();
using (OdfPackage writer = OdfDocumentFactory.CreatePackage(original, OdfDocumentKind.Text, version, true))
{
writer.Save();
}
original.Position = 0;
using (OdfPackage loaded = OdfPackage.Open(original, true))
{
Assert.Equal(version, loaded.Version);
using var saved = new MemoryStream();
loaded.SaveToStream(saved);
saved.Position = 0;
using OdfPackage reloaded = OdfPackage.Open(saved);
Assert.Equal(version, reloaded.Version);
Assert.True(OdfPackageValidator.Validate(reloaded, version == OdfVersion.Odf10
? OdfComplianceProfiles.OasisOdf10 : OdfComplianceProfiles.OasisOdf11).IsValid);
}
original.Position = 0;
await using OdfPackage asyncLoaded = await OdfPackage.OpenAsync(original, leaveOpen: true,
cancellationToken: TestContext.Current.CancellationToken);
Assert.Equal(version, asyncLoaded.Version);
}
[Theory]
[InlineData("bar")]
[InlineData("chart:bar")]
public void ChartApiWritesNamespacedClassTokens(string chartClass)
{
using OdfChartDocument chart = OdfChartDocument.Create();
chart.ChartClass = chartClass;
Assert.Equal("chart:bar", chart.ChartClass);
using var stream = new MemoryStream();
chart.SaveToStream(stream);
stream.Position = 0;
using OdfPackage package = OdfPackage.Open(stream);
using Stream content = package.GetEntryStream("content.xml");
XElement element = XDocument.Load(content).Descendants(XName.Get("chart", OdfNamespaces.Chart)).Single();
OdfSchemaSet schema = OdfSchemaRegistry.GetSchema(OdfVersion.Odf14);
Assert.True(OdfSchemaPatternValidator.ValidateElement(element, schema, "chart-chart").IsMatch);
element.SetAttributeValue(XName.Get("class", OdfNamespaces.Chart), "bar");
Assert.False(OdfSchemaPatternValidator.ValidateElement(element, schema, "chart-chart").IsMatch);
}
[Fact]
public void ImageApiKeepsAccessibilityMetadataAfterImageContentWhenReplacingImage()
{
using ImageDocument image = ImageDocument.Create();
image.SetImageLayout(OdfLength.FromCentimeters(0), OdfLength.FromCentimeters(0),
OdfLength.FromCentimeters(1), OdfLength.FromCentimeters(1), "Image", "Title", "Description");
byte[] bytes = Convert.FromBase64String("iVBORw0KGgoAAAANSUhEUgAAAAEAAAABCAQAAAC1HAwCAAAAC0lEQVR42mP8/x8AAwMCAO+j8ioAAAAASUVORK5CYII=");
image.SetImage(bytes, "first.png");
image.SetImage(bytes, "second.png");
using var stream = new MemoryStream();
image.SaveToStream(stream);
stream.Position = 0;
using OdfPackage package = OdfPackage.Open(stream);
using Stream content = package.GetEntryStream("content.xml");
XElement frame = XDocument.Load(content).Descendants(XName.Get("frame", OdfNamespaces.Draw)).Single();
OdfSchemaSet schema = OdfSchemaRegistry.GetSchema(OdfVersion.Odf14);
Assert.True(OdfSchemaPatternValidator.ValidateElement(frame, schema, "draw-frame").IsMatch);
Assert.Equal("Title", frame.Element(XName.Get("title", OdfNamespaces.Svg))?.Value);
XElement imageElement = Assert.Single(frame.Elements(XName.Get("image", OdfNamespaces.Draw)));
imageElement.Remove();
frame.Add(imageElement);
Assert.False(OdfSchemaPatternValidator.ValidateElement(frame, schema, "draw-frame").IsMatch);
}
[Theory]
[InlineData(OdfVersion.Odf12)]
[InlineData(OdfVersion.Odf13)]
[InlineData(OdfVersion.Odf14)]
public void InlineForeignContentIsValidatedInsteadOfDiscarded(OdfVersion version)
{
string valid = WrapForeignText(version, "beforeafter");
OdfValidationReport accepted = ValidateContent(valid, OdfDocumentKind.Text, version);
Assert.True(accepted.IsValid, string.Join("; ", accepted.Issues.Select(issue => issue.Message)));
string invalid = WrapForeignText(version, "illegal nested paragraph");
Assert.Contains(ValidateContent(invalid, OdfDocumentKind.Text, version).Issues, issue => issue.RuleId == "ODF3101");
}
[Theory]
[InlineData(OdfVersion.Odf12)]
[InlineData(OdfVersion.Odf13)]
[InlineData(OdfVersion.Odf14)]
public void ForeignContentRequiresAnUnbrokenTextPermittingAncestorChain(OdfVersion version)
{
string outside = WrapForeignText(version, "outside mixed text");
string blocked = WrapForeignText(version, "ignored foreign content");
foreach (string content in new[] { outside, blocked })
{
OdfValidationReport report = ValidateContent(content, OdfDocumentKind.Text, version);
Assert.True(report.IsValid, string.Join("; ", report.Issues.Select(issue => issue.Message)));
}
}
[Theory]
[InlineData(OdfVersion.Odf12)]
[InlineData(OdfVersion.Odf13)]
[InlineData(OdfVersion.Odf14)]
public void InlineForeignUnwrapPreservesQNameBindings(OdfVersion version)
{
const string Shape = "";
string valid = WrapForeignText(version, $"{Shape}");
OdfValidationReport report = ValidateContent(valid, OdfDocumentKind.Text, version);
Assert.True(report.IsValid, string.Join("; ", report.Issues.Select(issue => issue.Message)));
string unbound = valid.Replace(" xmlns:q='urn:test:drawing-engine'", string.Empty, StringComparison.Ordinal);
Assert.Contains(ValidateContent(unbound, OdfDocumentKind.Text, version).Issues, issue => issue.RuleId == "ODF3101");
}
private static string WrapForeignText(OdfVersion version, string body) =>
$"{body}";
private static OdfValidationReport ValidateContent(string content, OdfDocumentKind kind, OdfVersion version, string? styles = null, string? embedded = null)
{
using var stream = new MemoryStream();
using (OdfPackage writer = OdfDocumentFactory.CreatePackage(stream, kind, version, true))
{
writer.WriteEntry("content.xml", Encoding.UTF8.GetBytes(content), "text/xml");
if (styles is not null)
{
writer.WriteEntry("styles.xml", Encoding.UTF8.GetBytes(styles), "text/xml");
}
if (embedded is not null)
{
writer.WriteEntry("Object1/content.xml", Encoding.UTF8.GetBytes(embedded), "text/xml");
}
writer.Save();
}
stream.Position = 0;
using OdfPackage package = OdfPackage.Open(stream);
OdfComplianceProfile profile = version switch
{
OdfVersion.Odf10 => OdfComplianceProfiles.OasisOdf10,
OdfVersion.Odf11 => OdfComplianceProfiles.OasisOdf11,
OdfVersion.Odf12 => OdfComplianceProfiles.OasisOdf12Extended,
OdfVersion.Odf13 => OdfComplianceProfiles.OasisOdf13Extended,
_ => OdfComplianceProfiles.OasisOdf14Extended
};
byte[] original = (byte[])package.ReadEntry("content.xml").Clone();
OdfValidationReport report = OdfPackageValidator.Validate(package, profile);
Assert.Equal(original, package.ReadEntry("content.xml"));
return report;
}
}