encapsulate path utilities for reuse, add unit tests
This commit is contained in:
parent
049952de33
commit
3b4e81bf69
|
@ -0,0 +1,70 @@
|
|||
using NUnit.Framework;
|
||||
using StardewModdingAPI.Framework.Utilities;
|
||||
|
||||
namespace StardewModdingAPI.Tests.Core
|
||||
{
|
||||
/// <summary>Unit tests for <see cref="PathUtilities"/>.</summary>
|
||||
[TestFixture]
|
||||
public class PathUtilitiesTests
|
||||
{
|
||||
/*********
|
||||
** Unit tests
|
||||
*********/
|
||||
[Test(Description = "Assert that GetSegments returns the expected values.")]
|
||||
[TestCase("", ExpectedResult = "")]
|
||||
[TestCase("/", ExpectedResult = "")]
|
||||
[TestCase("///", ExpectedResult = "")]
|
||||
[TestCase("/usr/bin", ExpectedResult = "usr|bin")]
|
||||
[TestCase("/usr//bin//", ExpectedResult = "usr|bin")]
|
||||
[TestCase("/usr//bin//.././boop.exe", ExpectedResult = "usr|bin|..|.|boop.exe")]
|
||||
[TestCase(@"C:", ExpectedResult = "C:")]
|
||||
[TestCase(@"C:/boop", ExpectedResult = "C:|boop")]
|
||||
[TestCase(@"C:\boop\/usr//bin//.././boop.exe", ExpectedResult = "C:|boop|usr|bin|..|.|boop.exe")]
|
||||
public string GetSegments(string path)
|
||||
{
|
||||
return string.Join("|", PathUtilities.GetSegments(path));
|
||||
}
|
||||
|
||||
[Test(Description = "Assert that NormalisePathSeparators returns the expected values.")]
|
||||
#if SMAPI_FOR_WINDOWS
|
||||
[TestCase("", ExpectedResult = "")]
|
||||
[TestCase("/", ExpectedResult = "")]
|
||||
[TestCase("///", ExpectedResult = "")]
|
||||
[TestCase("/usr/bin", ExpectedResult = @"usr\bin")]
|
||||
[TestCase("/usr//bin//", ExpectedResult = @"usr\bin")]
|
||||
[TestCase("/usr//bin//.././boop.exe", ExpectedResult = @"usr\bin\..\.\boop.exe")]
|
||||
[TestCase("C:", ExpectedResult = "C:")]
|
||||
[TestCase("C:/boop", ExpectedResult = @"C:\boop")]
|
||||
[TestCase(@"C:\usr\bin//.././boop.exe", ExpectedResult = @"C:\usr\bin\..\.\boop.exe")]
|
||||
#else
|
||||
[TestCase("", ExpectedResult = "")]
|
||||
[TestCase("/", ExpectedResult = "/")]
|
||||
[TestCase("///", ExpectedResult = "/")]
|
||||
[TestCase("/usr/bin", ExpectedResult = "/usr/bin")]
|
||||
[TestCase("/usr//bin//", ExpectedResult = "/usr/bin")]
|
||||
[TestCase("/usr//bin//.././boop.exe", ExpectedResult = "/usr/bin/.././boop.exe")]
|
||||
[TestCase("C:", ExpectedResult = "C:")]
|
||||
[TestCase("C:/boop", ExpectedResult = "C:/boop")]
|
||||
[TestCase(@"C:\usr\bin//.././boop.exe", ExpectedResult = "C:/usr/bin/.././boop.exe")]
|
||||
#endif
|
||||
public string NormalisePathSeparators(string path)
|
||||
{
|
||||
return PathUtilities.NormalisePathSeparators(path);
|
||||
}
|
||||
|
||||
[Test(Description = "Assert that GetRelativePath returns the expected values.")]
|
||||
#if SMAPI_FOR_WINDOWS
|
||||
[TestCase(@"C:\", @"C:\", ExpectedResult = "./")]
|
||||
[TestCase(@"C:\grandparent\parent\child", @"C:\grandparent\parent\sibling", ExpectedResult = @"..\sibling")]
|
||||
[TestCase(@"C:\grandparent\parent\child", @"C:\cousin\file.exe", ExpectedResult = @"..\..\..\cousin\file.exe")]
|
||||
#else
|
||||
[TestCase("/", "/", ExpectedResult = "./")]
|
||||
[TestCase("/grandparent/parent/child", "/grandparent/parent/sibling", ExpectedResult = "../sibling")]
|
||||
[TestCase("/grandparent/parent/child", "/cousin/file.exe", ExpectedResult = "../../../cousin/file.exe")]
|
||||
#endif
|
||||
public string GetRelativePath(string sourceDir, string targetPath)
|
||||
{
|
||||
return PathUtilities.GetRelativePath(sourceDir, targetPath);
|
||||
}
|
||||
}
|
||||
}
|
|
@ -49,6 +49,7 @@
|
|||
<Compile Include="..\..\build\GlobalAssemblyInfo.cs">
|
||||
<Link>Properties\GlobalAssemblyInfo.cs</Link>
|
||||
</Compile>
|
||||
<Compile Include="Core\PathUtilitiesTests.cs" />
|
||||
<Compile Include="Utilities\SemanticVersionTests.cs" />
|
||||
<Compile Include="Utilities\SDateTests.cs" />
|
||||
<Compile Include="Core\TranslationTests.cs" />
|
||||
|
|
|
@ -5,6 +5,7 @@ using System.Linq;
|
|||
using Microsoft.Xna.Framework;
|
||||
using StardewModdingAPI.Framework.ModLoading;
|
||||
using StardewModdingAPI.Framework.Reflection;
|
||||
using StardewModdingAPI.Framework.Utilities;
|
||||
using StardewValley;
|
||||
|
||||
namespace StardewModdingAPI.Framework.Content
|
||||
|
@ -18,12 +19,6 @@ namespace StardewModdingAPI.Framework.Content
|
|||
/// <summary>The underlying asset cache.</summary>
|
||||
private readonly IDictionary<string, object> Cache;
|
||||
|
||||
/// <summary>The possible directory separator characters in an asset key.</summary>
|
||||
private readonly char[] PossiblePathSeparators;
|
||||
|
||||
/// <summary>The preferred directory separator chaeacter in an asset key.</summary>
|
||||
private readonly string PreferredPathSeparator;
|
||||
|
||||
/// <summary>Applies platform-specific asset key normalisation so it's consistent with the underlying cache.</summary>
|
||||
private readonly Func<string, string> NormaliseAssetNameForPlatform;
|
||||
|
||||
|
@ -52,14 +47,10 @@ namespace StardewModdingAPI.Framework.Content
|
|||
/// <summary>Construct an instance.</summary>
|
||||
/// <param name="contentManager">The underlying content manager whose cache to manage.</param>
|
||||
/// <param name="reflection">Simplifies access to private game code.</param>
|
||||
/// <param name="possiblePathSeparators">The possible directory separator characters in an asset key.</param>
|
||||
/// <param name="preferredPathSeparator">The preferred directory separator chaeacter in an asset key.</param>
|
||||
public ContentCache(LocalizedContentManager contentManager, Reflector reflection, char[] possiblePathSeparators, string preferredPathSeparator)
|
||||
public ContentCache(LocalizedContentManager contentManager, Reflector reflection)
|
||||
{
|
||||
// init
|
||||
this.Cache = reflection.GetField<Dictionary<string, object>>(contentManager, "loadedAssets").GetValue();
|
||||
this.PossiblePathSeparators = possiblePathSeparators;
|
||||
this.PreferredPathSeparator = preferredPathSeparator;
|
||||
|
||||
// get key normalisation logic
|
||||
if (Constants.TargetPlatform == Platform.Windows)
|
||||
|
@ -90,11 +81,7 @@ namespace StardewModdingAPI.Framework.Content
|
|||
[Pure]
|
||||
public string NormalisePathSeparators(string path)
|
||||
{
|
||||
string[] parts = path.Split(this.PossiblePathSeparators, StringSplitOptions.RemoveEmptyEntries);
|
||||
string normalised = string.Join(this.PreferredPathSeparator, parts);
|
||||
if (path.StartsWith(this.PreferredPathSeparator))
|
||||
normalised = this.PreferredPathSeparator + normalised; // keep root slash
|
||||
return normalised;
|
||||
return PathUtilities.NormalisePathSeparators(path);
|
||||
}
|
||||
|
||||
/// <summary>Normalise a cache key so it's consistent with the underlying cache.</summary>
|
||||
|
|
|
@ -8,6 +8,7 @@ using System.Linq;
|
|||
using Microsoft.Xna.Framework.Content;
|
||||
using Microsoft.Xna.Framework.Graphics;
|
||||
using StardewModdingAPI.Framework.Exceptions;
|
||||
using StardewModdingAPI.Framework.Utilities;
|
||||
using StardewValley;
|
||||
using xTile;
|
||||
using xTile.Format;
|
||||
|
@ -238,7 +239,7 @@ namespace StardewModdingAPI.Framework.ModHelpers
|
|||
string imageSource = tilesheet.ImageSource;
|
||||
|
||||
// validate tilesheet path
|
||||
if (Path.IsPathRooted(imageSource) || imageSource.Split(SContentManager.PossiblePathSeparators).Contains(".."))
|
||||
if (Path.IsPathRooted(imageSource) || PathUtilities.GetSegments(imageSource).Contains(".."))
|
||||
throw new ContentLoadException($"The '{imageSource}' tilesheet couldn't be loaded. Tilesheet paths must be a relative path without directory climbing (../).");
|
||||
|
||||
// get seasonal name (if applicable)
|
||||
|
|
|
@ -35,9 +35,6 @@ namespace StardewModdingAPI.Framework
|
|||
/*********
|
||||
** Properties
|
||||
*********/
|
||||
/// <summary>The preferred directory separator chaeacter in an asset key.</summary>
|
||||
private static readonly string PreferredPathSeparator = Path.DirectorySeparatorChar.ToString();
|
||||
|
||||
/// <summary>Encapsulates monitoring and logging.</summary>
|
||||
private readonly IMonitor Monitor;
|
||||
|
||||
|
@ -75,9 +72,6 @@ namespace StardewModdingAPI.Framework
|
|||
/// <summary>Interceptors which edit matching assets after they're loaded.</summary>
|
||||
internal IDictionary<IModMetadata, IList<IAssetEditor>> Editors { get; } = new Dictionary<IModMetadata, IList<IAssetEditor>>();
|
||||
|
||||
/// <summary>The possible directory separator characters in an asset key.</summary>
|
||||
internal static readonly char[] PossiblePathSeparators = new[] { '/', '\\', Path.DirectorySeparatorChar, Path.AltDirectorySeparatorChar }.Distinct().ToArray();
|
||||
|
||||
/// <summary>The absolute path to the <see cref="ContentManager.RootDirectory"/>.</summary>
|
||||
internal string FullRootDirectory => Path.Combine(Constants.ExecutionPath, this.RootDirectory);
|
||||
|
||||
|
@ -100,7 +94,7 @@ namespace StardewModdingAPI.Framework
|
|||
{
|
||||
// init
|
||||
this.Monitor = monitor ?? throw new ArgumentNullException(nameof(monitor));
|
||||
this.Cache = new ContentCache(this, reflection, SContentManager.PossiblePathSeparators, SContentManager.PreferredPathSeparator);
|
||||
this.Cache = new ContentCache(this, reflection);
|
||||
this.GetKeyLocale = reflection.GetMethod(this, "languageCode");
|
||||
this.ModContentPrefix = this.GetAssetNameFromFilePath(Constants.ModPath);
|
||||
|
||||
|
@ -399,15 +393,7 @@ namespace StardewModdingAPI.Framework
|
|||
/// <param name="targetPath">The target file path.</param>
|
||||
private string GetRelativePath(string targetPath)
|
||||
{
|
||||
// convert to URIs
|
||||
Uri from = new Uri(this.FullRootDirectory + "/");
|
||||
Uri to = new Uri(targetPath + "/");
|
||||
if (from.Scheme != to.Scheme)
|
||||
throw new InvalidOperationException($"Can't get path for '{targetPath}' relative to '{this.FullRootDirectory}'.");
|
||||
|
||||
// get relative path
|
||||
return Uri.UnescapeDataString(from.MakeRelativeUri(to).ToString())
|
||||
.Replace(Path.DirectorySeparatorChar == '/' ? '\\' : '/', Path.DirectorySeparatorChar); // use correct separator for platform
|
||||
return PathUtilities.GetRelativePath(this.FullRootDirectory, targetPath);
|
||||
}
|
||||
|
||||
/// <summary>Get the locale codes (like <c>ja-JP</c>) used in asset keys.</summary>
|
||||
|
|
|
@ -0,0 +1,59 @@
|
|||
using System;
|
||||
using System.Diagnostics.Contracts;
|
||||
using System.IO;
|
||||
using System.Linq;
|
||||
|
||||
namespace StardewModdingAPI.Framework.Utilities
|
||||
{
|
||||
/// <summary>Provides utilities for normalising file paths.</summary>
|
||||
internal static class PathUtilities
|
||||
{
|
||||
/*********
|
||||
** Properties
|
||||
*********/
|
||||
/// <summary>The possible directory separator characters in a file path.</summary>
|
||||
private static readonly char[] PossiblePathSeparators = new[] { '/', '\\', Path.DirectorySeparatorChar, Path.AltDirectorySeparatorChar }.Distinct().ToArray();
|
||||
|
||||
/// <summary>The preferred directory separator chaeacter in an asset key.</summary>
|
||||
private static readonly string PreferredPathSeparator = Path.DirectorySeparatorChar.ToString();
|
||||
|
||||
|
||||
/*********
|
||||
** Public methods
|
||||
*********/
|
||||
/// <summary>Get the segments from a path (e.g. <c>/usr/bin/boop</c> => <c>usr</c>, <c>bin</c>, and <c>boop</c>).</summary>
|
||||
/// <param name="path">The path to split.</param>
|
||||
public static string[] GetSegments(string path)
|
||||
{
|
||||
return path.Split(PathUtilities.PossiblePathSeparators, StringSplitOptions.RemoveEmptyEntries);
|
||||
}
|
||||
|
||||
/// <summary>Normalise path separators in a file path.</summary>
|
||||
/// <param name="path">The file path to normalise.</param>
|
||||
[Pure]
|
||||
public static string NormalisePathSeparators(string path)
|
||||
{
|
||||
string[] parts = PathUtilities.GetSegments(path);
|
||||
string normalised = string.Join(PathUtilities.PreferredPathSeparator, parts);
|
||||
if (path.StartsWith(PathUtilities.PreferredPathSeparator))
|
||||
normalised = PathUtilities.PreferredPathSeparator + normalised; // keep root slash
|
||||
return normalised;
|
||||
}
|
||||
|
||||
/// <summary>Get a directory or file path relative to a given source path.</summary>
|
||||
/// <param name="sourceDir">The source folder path.</param>
|
||||
/// <param name="targetPath">The target folder or file path.</param>
|
||||
[Pure]
|
||||
public static string GetRelativePath(string sourceDir, string targetPath)
|
||||
{
|
||||
// convert to URIs
|
||||
Uri from = new Uri(sourceDir.TrimEnd(PathUtilities.PossiblePathSeparators) + "/");
|
||||
Uri to = new Uri(targetPath.TrimEnd(PathUtilities.PossiblePathSeparators) + "/");
|
||||
if (from.Scheme != to.Scheme)
|
||||
throw new InvalidOperationException($"Can't get path for '{targetPath}' relative to '{sourceDir}'.");
|
||||
|
||||
// get relative path
|
||||
return PathUtilities.NormalisePathSeparators(Uri.UnescapeDataString(from.MakeRelativeUri(to).ToString()));
|
||||
}
|
||||
}
|
||||
}
|
|
@ -125,6 +125,7 @@
|
|||
<Compile Include="Framework\Serialisation\CrossplatformConverters\ColorConverter.cs" />
|
||||
<Compile Include="Framework\Serialisation\CrossplatformConverters\PointConverter.cs" />
|
||||
<Compile Include="Framework\Utilities\ContextHash.cs" />
|
||||
<Compile Include="Framework\Utilities\PathUtilities.cs" />
|
||||
<Compile Include="IContentPack.cs" />
|
||||
<Compile Include="IManifestContentPackFor.cs" />
|
||||
<Compile Include="IReflectedField.cs" />
|
||||
|
|
Loading…
Reference in New Issue