From 57bc71c7eb2e9c0145cae454424d53ca544f06e1 Mon Sep 17 00:00:00 2001 From: Jesse Plamondon-Willard Date: Tue, 15 Sep 2020 17:34:14 -0400 Subject: make IContentPack file paths case-insensitive --- src/SMAPI/Framework/ContentPack.cs | 66 ++++++++++++++++++++++++++++++-------- 1 file changed, 52 insertions(+), 14 deletions(-) (limited to 'src/SMAPI/Framework/ContentPack.cs') diff --git a/src/SMAPI/Framework/ContentPack.cs b/src/SMAPI/Framework/ContentPack.cs index 65abba5b..161fdbe4 100644 --- a/src/SMAPI/Framework/ContentPack.cs +++ b/src/SMAPI/Framework/ContentPack.cs @@ -1,4 +1,5 @@ using System; +using System.Collections.Generic; using System.IO; using StardewModdingAPI.Toolkit.Serialization; using StardewModdingAPI.Toolkit.Utilities; @@ -17,6 +18,9 @@ namespace StardewModdingAPI.Framework /// Encapsulates SMAPI's JSON file parsing. private readonly JsonHelper JsonHelper; + /// A cache of case-insensitive => exact relative paths within the content pack, for case-insensitive file lookups on Linux/Mac. + private readonly IDictionary RelativePaths = new Dictionary(StringComparer.OrdinalIgnoreCase); + /********* ** Accessors @@ -47,23 +51,29 @@ namespace StardewModdingAPI.Framework this.Content = content; this.Translation = translation; this.JsonHelper = jsonHelper; + + foreach (string path in Directory.EnumerateFiles(this.DirectoryPath, "*", SearchOption.AllDirectories)) + { + string relativePath = path.Substring(this.DirectoryPath.Length + 1); + this.RelativePaths[relativePath] = relativePath; + } } /// public bool HasFile(string path) { - this.AssertRelativePath(path, nameof(this.HasFile)); + path = PathUtilities.NormalizePath(path); - return File.Exists(Path.Combine(this.DirectoryPath, path)); + return this.GetFile(path).Exists; } /// public TModel ReadJsonFile(string path) where TModel : class { - this.AssertRelativePath(path, nameof(this.ReadJsonFile)); + path = PathUtilities.NormalizePath(path); - path = Path.Combine(this.DirectoryPath, PathUtilities.NormalizePath(path)); - return this.JsonHelper.ReadJsonFileIfExists(path, out TModel model) + FileInfo file = this.GetFile(path); + return file.Exists && this.JsonHelper.ReadJsonFileIfExists(file.FullName, out TModel model) ? model : null; } @@ -71,21 +81,30 @@ namespace StardewModdingAPI.Framework /// public void WriteJsonFile(string path, TModel data) where TModel : class { - this.AssertRelativePath(path, nameof(this.WriteJsonFile)); + path = PathUtilities.NormalizePath(path); + + FileInfo file = this.GetFile(path, out path); + this.JsonHelper.WriteJsonFile(file.FullName, data); - path = Path.Combine(this.DirectoryPath, PathUtilities.NormalizePath(path)); - this.JsonHelper.WriteJsonFile(path, data); + if (!this.RelativePaths.ContainsKey(path)) + this.RelativePaths[path] = path; } /// public T LoadAsset(string key) { + key = PathUtilities.NormalizePath(key); + + key = this.GetCaseInsensitiveRelativePath(key); return this.Content.Load(key, ContentSource.ModFolder); } /// public string GetActualAssetKey(string key) { + key = PathUtilities.NormalizePath(key); + + key = this.GetCaseInsensitiveRelativePath(key); return this.Content.GetActualAssetKey(key, ContentSource.ModFolder); } @@ -93,13 +112,32 @@ namespace StardewModdingAPI.Framework /********* ** Private methods *********/ - /// Assert that a relative path was passed it to a content pack method. - /// The path to check. - /// The name of the method which was invoked. - private void AssertRelativePath(string path, string methodName) + /// Get the real relative path from a case-insensitive path. + /// The normalized relative path. + private string GetCaseInsensitiveRelativePath(string relativePath) + { + if (!PathUtilities.IsSafeRelativePath(relativePath)) + throw new InvalidOperationException($"You must call {nameof(IContentPack)} methods with a relative path."); + + return this.RelativePaths.TryGetValue(relativePath, out string caseInsensitivePath) + ? caseInsensitivePath + : relativePath; + } + + /// Get the underlying file info. + /// The normalized file path relative to the content pack directory. + private FileInfo GetFile(string relativePath) + { + return this.GetFile(relativePath, out _); + } + + /// Get the underlying file info. + /// The normalized file path relative to the content pack directory. + /// The relative path after case-insensitive matching. + private FileInfo GetFile(string relativePath, out string actualRelativePath) { - if (!PathUtilities.IsSafeRelativePath(path)) - throw new InvalidOperationException($"You must call {nameof(IContentPack)}.{methodName} with a relative path."); + actualRelativePath = this.GetCaseInsensitiveRelativePath(relativePath); + return new FileInfo(Path.Combine(this.DirectoryPath, actualRelativePath)); } } } -- cgit