Skip to content

Commit

Permalink
Packaging: Port fix for GetParts from .NETFramework (#108681)
Browse files Browse the repository at this point in the history
* Packaging: Port fix for GetParts from .NETFramework

This ports a fix for the `GetParts` method to improve part URI handling and collision detection.

`src/libraries/System.IO.Packaging/src/System/IO/Packaging/Package.cs`: Added sorting of parts array to ensure proper order, introduced a dictionary and list to track parts and detect collisions, and implemented a new method `CopyPartDictionaryToPartList` to copy parts to `_partList`.

* Fix regression in ExtensionEqualityComparer

Previously this comparison was Ordinal after calling ToUpperInvariant

This was changed to InvariantIgnoreCase which breaks because it will
treat things like ß and ss as equal where they were not before.

* Address feedback

* Remove servicing properties from IO.Packaging
  • Loading branch information
ericstj authored Oct 10, 2024
1 parent 72ebebf commit 5cc1eb5
Show file tree
Hide file tree
Showing 2 changed files with 58 additions and 8 deletions.
Original file line number Diff line number Diff line change
Expand Up @@ -380,6 +380,8 @@ public void DeletePart(Uri partUri)
/// <returns></returns>
/// <exception cref="ObjectDisposedException">If this Package object has been disposed</exception>
/// <exception cref="IOException">If the package is writeonly, no information can be retrieved from it</exception>
/// <exception cref="FileFormatException">The package has a bad format.</exception>
/// <exception cref="InvalidOperationException">The part name prefix exists.</exception>
public PackagePartCollection GetParts()
{
ThrowIfObjectDisposed();
Expand All @@ -401,32 +403,63 @@ public PackagePartCollection GetParts()

PackUriHelper.ValidatedPartUri partUri;

var uriComparer = Comparer<PackUriHelper.ValidatedPartUri>.Default;

//Sorting the parts array which takes O(n log n) time.
Array.Sort(parts, Comparer<PackagePart>.Create((partA, partB) => uriComparer.Compare((PackUriHelper.ValidatedPartUri)partA.Uri, (PackUriHelper.ValidatedPartUri)partB.Uri)));

//We need this dictionary to detect any collisions that might be present in the
//list of parts that was given to us from the underlying physical layer, as more than one
//partnames can be mapped to the same normalized part.
//Note: We cannot use the _partList member variable, as that gets updated incrementally and so its
//not possible to find the collisions using that list.
//PackUriHelper.ValidatedPartUri implements the IComparable interface.
Dictionary<PackUriHelper.ValidatedPartUri, PackagePart> seenPartUris = new Dictionary<PackUriHelper.ValidatedPartUri, PackagePart>(parts.Length);
Dictionary<string, KeyValuePair<PackUriHelper.ValidatedPartUri, PackagePart>> partDictionary = new(parts.Length);
List<string> partIndex = new(parts.Length);

for (int i = 0; i < parts.Length; i++)
{
partUri = (PackUriHelper.ValidatedPartUri)parts[i].Uri;

if (seenPartUris.ContainsKey(partUri))
string normalizedPartName = partUri.NormalizedPartUriString;

if (partDictionary.ContainsKey(normalizedPartName))
{
throw new FileFormatException(SR.BadPackageFormat);
}
else
{
// Add the part to the list of URIs that we have already seen
seenPartUris.Add(partUri, parts[i]);
//since we will arrive to this line of code after the parts are already sorted
string? precedingPartName = null;

if (partIndex.Count > 0)
{
precedingPartName = (partIndex[partIndex.Count - 1]);
}

// Add the part to the dictionary
partDictionary.Add(normalizedPartName, new KeyValuePair<PackUriHelper.ValidatedPartUri, PackagePart>(partUri, parts[i]));

if (!_partList.ContainsKey(partUri))
if (precedingPartName != null
&& normalizedPartName.StartsWith(precedingPartName, StringComparison.Ordinal)
&& normalizedPartName.Length > precedingPartName.Length
&& normalizedPartName[precedingPartName.Length] == PackUriHelper.ForwardSlashChar)
{
// Add the part to the _partList if there is no prefix collision
AddIfNoPrefixCollisionDetected(partUri, parts[i]);
//Removing the invalid entry from the _partList.
partDictionary.Remove(normalizedPartName);

throw new InvalidOperationException(SR.PartNamePrefixExists);
}

//adding entry to partIndex to keep track of last element being added.
//since parts are already sorted, last element in partIndex list will point to preceeding element to the current.
partIndex.Add(partUri.NormalizedPartUriString);
}
}

//copying parts from partdictionary to partlist
CopyPartDictionaryToPartList(partDictionary, partIndex);

_partCollection = new PackagePartCollection(_partList);
}
return _partCollection;
Expand Down Expand Up @@ -1173,6 +1206,23 @@ private PackageRelationshipCollection GetRelationshipsHelper(string? filterStrin
return new PackageRelationshipCollection(_relationships, filterString);
}

private void CopyPartDictionaryToPartList(Dictionary<string, KeyValuePair<PackUriHelper.ValidatedPartUri, PackagePart>> partDictionary, List<string> partIndex)
{
//Clearing _partList before copying in new data. Reassigning the variable, assuming the previous object to be garbage collected.
//ideally addition to sortedlist takes O(n) but since we have sorted data and also we defined the size, it will take O(log n) per addition
//total time complexity for this function will be O(n log n)
_partList = new SortedList<PackUriHelper.ValidatedPartUri, PackagePart>(partDictionary.Count);

//Since partIndex is created from a sorted parts array we are sure that partIndex
//will have items in same order
foreach (var id in partIndex)
{
//retrieving object from partDictionary hashtable
var keyValue = partDictionary[id];
_partList.Add(keyValue.Key, keyValue.Value);
}
}

#endregion Private Methods

#region Private Members
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -739,7 +739,7 @@ bool IEqualityComparer<string>.Equals(string? extensionA, string? extensionB)
//with the rules for comparing/normalizing partnames.
//Refer to PackUriHelper.ValidatedPartUri.GetNormalizedPartUri method.
//Currently normalization just involves upper-casing ASCII and hence the simplification.
return extensionA.Equals(extensionB, StringComparison.InvariantCultureIgnoreCase);
return extensionA.Equals(extensionB, StringComparison.OrdinalIgnoreCase);
}

int IEqualityComparer<string>.GetHashCode(string extension)
Expand Down

0 comments on commit 5cc1eb5

Please sign in to comment.