Skip to content
Original file line number Diff line number Diff line change
@@ -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;
Expand Down Expand Up @@ -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.
/// </summary>
#pragma warning disable CA1067 // Not overriding Equals(object); changing equality behavior could affect existing case-sensitive comparisons.
internal sealed class ValidatedPartUri : Uri, IComparable<ValidatedPartUri>, IEquatable<ValidatedPartUri>
#pragma warning restore CA1067
{
//------------------------------------------------------
//
Expand Down Expand Up @@ -651,22 +654,6 @@ bool IEquatable<ValidatedPartUri>.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

//------------------------------------------------------
Expand Down Expand Up @@ -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);
Comment thread
alinpahontu2912 marked this conversation as resolved.
}

//------------------------------------------------------
Expand Down Expand Up @@ -893,6 +876,37 @@ private int Compare(ValidatedPartUri? otherPartUri)
//------------------------------------------------------
}

/// <summary>
/// Compares <see cref="ValidatedPartUri"/> instances for equality using the normalized (upper-cased) part URI
/// string comparison implemented by <see cref="IEquatable{ValidatedPartUri}.Equals(ValidatedPartUri)"/>.
/// This is used explicitly by internal collections that need case-insensitive part-name
/// semantics (e.g. <see cref="ZipPackage"/>'s content-type override dictionary), without
/// relying on <see cref="ValidatedPartUri"/> overriding <see cref="object.Equals(object?)"/>
/// or <see cref="object.GetHashCode"/>, which would break the equality contract inherited
/// from <see cref="Uri"/> when a <see cref="ValidatedPartUri"/> is compared against, or
/// co-located in a hash collection with, a plain <see cref="Uri"/> of the same value.
/// </summary>
internal sealed class ValidatedPartUriEqualityComparer : IEqualityComparer<ValidatedPartUri>
{
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<ValidatedPartUri>)x).Equals(y);
}

public int GetHashCode(ValidatedPartUri obj)
=> StringComparer.Ordinal.GetHashCode(obj.NormalizedPartUriString);
}

#endregion Private Class
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -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<PackUriHelper.ValidatedPartUri, ContentType>(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<PackUriHelper.ValidatedPartUri, ContentType>(
OverrideDictionaryInitialSize,
PackUriHelper.ValidatedPartUriEqualityComparer.Instance);
}

private void ParseContentTypesFile(System.Collections.ObjectModel.ReadOnlyCollection<ZipArchiveEntry> zipFiles)
Expand Down
65 changes: 65 additions & 0 deletions src/libraries/System.IO.Packaging/tests/PartPieceTests.cs
Original file line number Diff line number Diff line change
Expand Up @@ -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<Uri>/Dictionary<Uri,_>.
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<Uri> { plain };
Assert.Contains(validated, set);

var set2 = new HashSet<Uri> { 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(
"""
<?xml version="1.0" encoding="utf-8"?>
<Types xmlns="http://schemas.openxmlformats.org/package/2006/content-types">
<Override PartName="/Test.xml" ContentType="application/foo" />
</Types>
""");
}

var partEntry = zipArchive.CreateEntry("test.xml");
using (var writer = new StreamWriter(partEntry.Open()))
{
writer.Write("<root/>");
}
}

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()
{
Expand Down
Loading