diff --git a/src/libraries/System.IO.Packaging/src/System/IO/Packaging/PackUriHelper.cs b/src/libraries/System.IO.Packaging/src/System/IO/Packaging/PackUriHelper.cs index 19ed7644e1cecc..0f9553d84b61f3 100644 --- a/src/libraries/System.IO.Packaging/src/System/IO/Packaging/PackUriHelper.cs +++ b/src/libraries/System.IO.Packaging/src/System/IO/Packaging/PackUriHelper.cs @@ -1,6 +1,7 @@ // Licensed to the .NET Foundation under one or more agreements. // The .NET Foundation licenses this file to you under the MIT license. +using System.Collections.Generic; using System.Diagnostics; using System.Diagnostics.CodeAnalysis; using System.Text; @@ -601,7 +602,9 @@ private static bool IsPartNameEmpty(string partName) /// to reduce the parsing and number of allocations for Strings and Uris /// we cache the results after parsing. /// +#pragma warning disable CA1067 // Not overriding Equals(object); changing equality behavior could affect existing case-sensitive comparisons. internal sealed class ValidatedPartUri : Uri, IComparable, IEquatable +#pragma warning restore CA1067 { //------------------------------------------------------ // @@ -651,22 +654,6 @@ bool IEquatable.Equals(ValidatedPartUri? otherPartUri) #endregion IEquatable Methods - #region Overrides - - public override bool Equals(object? obj) - { - if (obj is ValidatedPartUri other) - return Compare(other) == 0; - return false; - } - - public override int GetHashCode() - { - return StringComparer.OrdinalIgnoreCase.GetHashCode(NormalizedPartUriString); - } - - #endregion Overrides - #region Internal Properties //------------------------------------------------------ @@ -855,11 +842,7 @@ private int Compare(ValidatedPartUri? otherPartUri) return 1; //Compare the normalized uri strings for the two part uris. - return string.Compare( - NormalizedPartUriString, - otherPartUri.NormalizedPartUriString, - StringComparison.OrdinalIgnoreCase - ); + return string.CompareOrdinal(NormalizedPartUriString, otherPartUri.NormalizedPartUriString); } //------------------------------------------------------ @@ -893,6 +876,37 @@ private int Compare(ValidatedPartUri? otherPartUri) //------------------------------------------------------ } + /// + /// Compares instances for equality using the normalized (upper-cased) part URI + /// string comparison implemented by . + /// This is used explicitly by internal collections that need case-insensitive part-name + /// semantics (e.g. 's content-type override dictionary), without + /// relying on overriding + /// or , which would break the equality contract inherited + /// from when a is compared against, or + /// co-located in a hash collection with, a plain of the same value. + /// + internal sealed class ValidatedPartUriEqualityComparer : IEqualityComparer + { + internal static readonly ValidatedPartUriEqualityComparer Instance = new(); + + private ValidatedPartUriEqualityComparer() { } + + public bool Equals(ValidatedPartUri? x, ValidatedPartUri? y) + { + if (ReferenceEquals(x, y)) + return true; + + if (x is null || y is null) + return false; + + return ((IEquatable)x).Equals(y); + } + + public int GetHashCode(ValidatedPartUri obj) + => StringComparer.Ordinal.GetHashCode(obj.NormalizedPartUriString); + } + #endregion Private Class } } diff --git a/src/libraries/System.IO.Packaging/src/System/IO/Packaging/ZipPackage.cs b/src/libraries/System.IO.Packaging/src/System/IO/Packaging/ZipPackage.cs index f3e1a416e360b6..e3caa3d2102a14 100644 --- a/src/libraries/System.IO.Packaging/src/System/IO/Packaging/ZipPackage.cs +++ b/src/libraries/System.IO.Packaging/src/System/IO/Packaging/ZipPackage.cs @@ -945,9 +945,11 @@ internal void SaveToFile() [MemberNotNull(nameof(_overrideDictionary))] private void EnsureOverrideDictionary() { - // The part Uris are stored in the Override Dictionary in their original form , but they are compared - // in a normalized manner using the PartUriComparer - _overrideDictionary ??= new Dictionary(OverrideDictionaryInitialSize); + // The part Uris are stored in the Override Dictionary in their original form, but they are compared + // in a normalized, case-insensitive manner using ValidatedPartUriEqualityComparer. + _overrideDictionary ??= new Dictionary( + OverrideDictionaryInitialSize, + PackUriHelper.ValidatedPartUriEqualityComparer.Instance); } private void ParseContentTypesFile(System.Collections.ObjectModel.ReadOnlyCollection zipFiles) diff --git a/src/libraries/System.IO.Packaging/tests/PartPieceTests.cs b/src/libraries/System.IO.Packaging/tests/PartPieceTests.cs index 37736f4d6d7f6f..d7011327ad1105 100644 --- a/src/libraries/System.IO.Packaging/tests/PartPieceTests.cs +++ b/src/libraries/System.IO.Packaging/tests/PartPieceTests.cs @@ -361,6 +361,71 @@ public void DuplicatePartsDifferingOnlyByCaseAreNotAllowed() } } + [Fact] + public void PartUriHonorsSystemUriEqualityContract() + { + // PackUriHelper.CreatePartUri returns an internal Uri subclass (ValidatedPartUri) that must + // preserve System.Uri's object.Equals/GetHashCode contract so it can be safely mixed with + // plain System.Uri instances in hash-based collections such as HashSet/Dictionary. + Uri plain = new Uri("/foo.xml", UriKind.Relative); + Uri validated = PackUriHelper.CreatePartUri(plain); + + object a = validated; + object b = plain; + + // object.Equals must be symmetric and treat a value-equal plain System.Uri as equal. + Assert.True(a.Equals(b)); + Assert.True(b.Equals(a)); + + // GetHashCode must be consistent with a value-equal plain System.Uri so both types can + // coexist as keys in the same hash-based collection. + Assert.Equal(plain.GetHashCode(), validated.GetHashCode()); + + var set = new HashSet { plain }; + Assert.Contains(validated, set); + + var set2 = new HashSet { validated }; + Assert.Contains(plain, set2); + } + + [Fact] + public void ContentTypeOverrideLookupIsCaseInsensitive() + { + // Regression test: a package whose [Content_Types].xml Override PartName differs only + // by case from the actual zip entry name must still resolve the part's content type. + // This exercises ZipPackage's internal ValidatedPartUri-keyed override dictionary, which + // must remain case-insensitive independent of whether ValidatedPartUri overrides + // object.Equals/GetHashCode. + using var ms = new MemoryStream(); + using (var zipArchive = new ZipArchive(ms, ZipArchiveMode.Create, leaveOpen: true)) + { + var contentTypesEntry = zipArchive.CreateEntry("[Content_Types].xml"); + using (var writer = new StreamWriter(contentTypesEntry.Open())) + { + writer.Write( + """ + + + + + """); + } + + var partEntry = zipArchive.CreateEntry("test.xml"); + using (var writer = new StreamWriter(partEntry.Open())) + { + writer.Write(""); + } + } + + ms.Position = 0; + using var package = Package.Open(ms, FileMode.Open, FileAccess.Read); + PackagePart[] parts = package.GetParts().ToArray(); + + Assert.Single(parts); + Assert.Equal("application/foo", parts[0].ContentType); + } + [Fact] public void CanCreateAtomicPart() {