fix case where prefix ends with a path separator

This commit is contained in:
Jesse Plamondon-Willard 2022-11-10 21:50:01 -05:00
parent b99dbf53bd
commit 303b3924ae
No known key found for this signature in database
GPG Key ID: CF8B1456B3E29F49
2 changed files with 19 additions and 12 deletions

View File

@ -151,6 +151,12 @@ namespace SMAPI.Tests.Core
// with locale codes // with locale codes
[TestCase("Data/Achievements.fr-FR", "Data/Achievements", ExpectedResult = true)] [TestCase("Data/Achievements.fr-FR", "Data/Achievements", ExpectedResult = true)]
// prefix ends with path separator
[TestCase("Data/Events/Boop", "Data/Events/", ExpectedResult = true)]
[TestCase("Data/Events/Boop", "Data/Events\\", ExpectedResult = true)]
[TestCase("Data/Events", "Data/Events/", ExpectedResult = false)]
[TestCase("Data/Events", "Data/Events\\", ExpectedResult = false)]
public bool StartsWith_SimpleCases(string mainAssetName, string prefix) public bool StartsWith_SimpleCases(string mainAssetName, string prefix)
{ {
// arrange // arrange
@ -247,7 +253,7 @@ namespace SMAPI.Tests.Core
[TestCase("Mods/SomeMod/SomeSubdirectory", "Mods/Some", false, ExpectedResult = false)] [TestCase("Mods/SomeMod/SomeSubdirectory", "Mods/Some", false, ExpectedResult = false)]
[TestCase("Mods/Jasper/Data", "Mods/Jas/Image", true, ExpectedResult = false)] [TestCase("Mods/Jasper/Data", "Mods/Jas/Image", true, ExpectedResult = false)]
[TestCase("Mods/Jasper/Data", "Mods/Jas/Image", true, ExpectedResult = false)] [TestCase("Mods/Jasper/Data", "Mods/Jas/Image", true, ExpectedResult = false)]
public bool StartsWith_SubfolderWithPartial(string mainAssetName, string otherAssetName, bool allowSubfolder) public bool StartsWith_PartialMatchInPathSegment(string mainAssetName, string otherAssetName, bool allowSubfolder)
{ {
// arrange // arrange
mainAssetName = PathUtilities.NormalizeAssetName(mainAssetName); mainAssetName = PathUtilities.NormalizeAssetName(mainAssetName);

View File

@ -138,37 +138,38 @@ namespace StardewModdingAPI.Framework.Content
return false; return false;
// get initial values // get initial values
ReadOnlySpan<char> trimmed = prefix.AsSpan().Trim(); ReadOnlySpan<char> trimmedPrefix = prefix.AsSpan().Trim();
if (trimmed.Length == 0) if (trimmedPrefix.Length == 0)
return true; return true;
ReadOnlySpan<char> pathSeparators = new(ToolkitPathUtilities.PossiblePathSeparators); // just to simplify calling other span APIs ReadOnlySpan<char> pathSeparators = new(ToolkitPathUtilities.PossiblePathSeparators); // just to simplify calling other span APIs
// asset keys can't have a leading slash, but AssetPathYielder will trim them // asset keys can't have a leading slash, but AssetPathYielder will trim them
if (pathSeparators.Contains(trimmed[0])) if (pathSeparators.Contains(trimmedPrefix[0]))
return false; return false;
// compare segments // compare segments
AssetNamePartEnumerator curParts = new(this.Name); AssetNamePartEnumerator curParts = new(this.Name);
AssetNamePartEnumerator prefixParts = new(trimmed); AssetNamePartEnumerator prefixParts = new(trimmedPrefix);
while (true) while (true)
{ {
bool curHasMore = curParts.MoveNext(); bool curHasMore = curParts.MoveNext();
bool prefixHasMore = prefixParts.MoveNext(); bool prefixHasMore = prefixParts.MoveNext();
// reached end of prefix or asset name // reached end for one side
if (prefixHasMore != curHasMore) if (prefixHasMore != curHasMore)
{ {
// mismatch: prefix is longer // mismatch: prefix is longer
if (prefixHasMore) if (prefixHasMore)
return false; return false;
// possible match: all prefix segments matched. // match if subfolder paths are fine (e.g. prefix 'Data/Events' with target 'Data/Events/Beach')
return allowSubfolder || (pathSeparators.Contains(trimmed[^1]) ? curParts.Remainder.Length == 0 : curParts.Current.Length == 0); return allowSubfolder;
} }
// match: previous segments matched exactly and both reached the end // previous segments matched exactly and both reached the end
// match if prefix doesn't end with '/' (which should only match subfolders)
if (!prefixHasMore) if (!prefixHasMore)
return true; return !pathSeparators.Contains(trimmedPrefix[^1]);
// compare segment // compare segment
if (curParts.Current.Length == prefixParts.Current.Length) if (curParts.Current.Length == prefixParts.Current.Length)
@ -188,7 +189,7 @@ namespace StardewModdingAPI.Framework.Content
return false; return false;
// mismatch: something like "Maps/" would need an exact match // mismatch: something like "Maps/" would need an exact match
if (pathSeparators.Contains(trimmed[^1])) if (pathSeparators.Contains(trimmedPrefix[^1]))
return false; return false;
// mismatch: partial word match not allowed, and the first or last letter of the suffix isn't a word separator // mismatch: partial word match not allowed, and the first or last letter of the suffix isn't a word separator
@ -196,7 +197,7 @@ namespace StardewModdingAPI.Framework.Content
return false; return false;
// possible match // possible match
return allowSubfolder || (pathSeparators.Contains(trimmed[^1]) ? curParts.Remainder.IndexOfAny(ToolkitPathUtilities.PossiblePathSeparators) < 0 : curParts.Remainder.Length == 0); return allowSubfolder || (pathSeparators.Contains(trimmedPrefix[^1]) ? curParts.Remainder.IndexOfAny(ToolkitPathUtilities.PossiblePathSeparators) < 0 : curParts.Remainder.Length == 0);
} }
} }
} }