diff options
Diffstat (limited to 'src/SMAPI.Toolkit')
-rw-r--r-- | src/SMAPI.Toolkit/Framework/LowLevelEnvironmentUtility.cs | 163 | ||||
-rw-r--r-- | src/SMAPI.Toolkit/SemanticVersion.cs | 47 | ||||
-rw-r--r-- | src/SMAPI.Toolkit/Utilities/EnvironmentUtility.cs | 139 | ||||
-rw-r--r-- | src/SMAPI.Toolkit/Utilities/PathUtilities.cs | 41 |
4 files changed, 226 insertions, 164 deletions
diff --git a/src/SMAPI.Toolkit/Framework/LowLevelEnvironmentUtility.cs b/src/SMAPI.Toolkit/Framework/LowLevelEnvironmentUtility.cs new file mode 100644 index 00000000..b01d8b21 --- /dev/null +++ b/src/SMAPI.Toolkit/Framework/LowLevelEnvironmentUtility.cs @@ -0,0 +1,163 @@ +using System; +using System.Diagnostics; +using System.Diagnostics.CodeAnalysis; +using System.Linq; +#if SMAPI_FOR_WINDOWS +using System.Management; +#endif +using System.Runtime.InteropServices; +using StardewModdingAPI.Toolkit.Utilities; + +namespace StardewModdingAPI.Toolkit.Framework +{ + /// <summary>Provides low-level methods for fetching environment information.</summary> + /// <remarks>This is used by the SMAPI core before the toolkit DLL is available; most code should use <see cref="EnvironmentUtility"/> instead.</remarks> + internal static class LowLevelEnvironmentUtility + { + /********* + ** Fields + *********/ + /// <summary>Get the OS name from the system uname command.</summary> + /// <param name="buffer">The buffer to fill with the resulting string.</param> + [DllImport("libc")] + static extern int uname(IntPtr buffer); + + + /********* + ** Public methods + *********/ + /// <summary>Detect the current OS.</summary> + public static string DetectPlatform() + { + switch (Environment.OSVersion.Platform) + { + case PlatformID.MacOSX: + return nameof(Platform.Mac); + + case PlatformID.Unix when LowLevelEnvironmentUtility.IsRunningAndroid(): + return nameof(Platform.Android); + + case PlatformID.Unix when LowLevelEnvironmentUtility.IsRunningMac(): + return nameof(Platform.Mac); + + case PlatformID.Unix: + return nameof(Platform.Linux); + + default: + return nameof(Platform.Windows); + } + } + + + /// <summary>Get the human-readable OS name and version.</summary> + /// <param name="platform">The current platform.</param> + [SuppressMessage("ReSharper", "EmptyGeneralCatchClause", Justification = "Error suppressed deliberately to fallback to default behaviour.")] + public static string GetFriendlyPlatformName(string platform) + { +#if SMAPI_FOR_WINDOWS + try + { + return new ManagementObjectSearcher("SELECT Caption FROM Win32_OperatingSystem") + .Get() + .Cast<ManagementObject>() + .Select(entry => entry.GetPropertyValue("Caption").ToString()) + .FirstOrDefault(); + } + catch { } +#endif + + string name = Environment.OSVersion.ToString(); + switch (platform) + { + case nameof(Platform.Android): + name = $"Android {name}"; + break; + + case nameof(Platform.Mac): + name = $"MacOS {name}"; + break; + } + return name; + } + + /// <summary>Get the name of the Stardew Valley executable.</summary> + /// <param name="platform">The current platform.</param> + public static string GetExecutableName(string platform) + { + return platform == nameof(Platform.Windows) + ? "Stardew Valley.exe" + : "StardewValley.exe"; + } + + /// <summary>Get whether the platform uses Mono.</summary> + /// <param name="platform">The current platform.</param> + public static bool IsMono(string platform) + { + return platform == nameof(Platform.Linux) || platform == nameof(Platform.Mac); + } + + + /********* + ** Private methods + *********/ + /// <summary>Detect whether the code is running on Android.</summary> + /// <remarks> + /// This code is derived from https://stackoverflow.com/a/47521647/262123. It detects Android by calling the + /// <c>getprop</c> system command to check for an Android-specific property. + /// </remarks> + private static bool IsRunningAndroid() + { + using Process process = new Process + { + StartInfo = + { + FileName = "getprop", + Arguments = "ro.build.user", + RedirectStandardOutput = true, + UseShellExecute = false, + CreateNoWindow = true + } + }; + + try + { + process.Start(); + string output = process.StandardOutput.ReadToEnd(); + return !string.IsNullOrWhiteSpace(output); + } + catch + { + return false; + } + } + + /// <summary>Detect whether the code is running on Mac.</summary> + /// <remarks> + /// This code is derived from the Mono project (see System.Windows.Forms/System.Windows.Forms/XplatUI.cs). It detects Mac by calling the + /// <c>uname</c> system command and checking the response, which is always 'Darwin' for MacOS. + /// </remarks> + private static bool IsRunningMac() + { + IntPtr buffer = IntPtr.Zero; + try + { + buffer = Marshal.AllocHGlobal(8192); + if (LowLevelEnvironmentUtility.uname(buffer) == 0) + { + string os = Marshal.PtrToStringAnsi(buffer); + return os == "Darwin"; + } + return false; + } + catch + { + return false; // default to Linux + } + finally + { + if (buffer != IntPtr.Zero) + Marshal.FreeHGlobal(buffer); + } + } + } +} diff --git a/src/SMAPI.Toolkit/SemanticVersion.cs b/src/SMAPI.Toolkit/SemanticVersion.cs index 1a76bec3..0f341665 100644 --- a/src/SMAPI.Toolkit/SemanticVersion.cs +++ b/src/SMAPI.Toolkit/SemanticVersion.cs @@ -25,22 +25,22 @@ namespace StardewModdingAPI.Toolkit /********* ** Accessors *********/ - /// <summary>The major version incremented for major API changes.</summary> + /// <inheritdoc /> public int MajorVersion { get; } - /// <summary>The minor version incremented for backwards-compatible changes.</summary> + /// <inheritdoc /> public int MinorVersion { get; } - /// <summary>The patch version for backwards-compatible bug fixes.</summary> + /// <inheritdoc /> public int PatchVersion { get; } /// <summary>The platform release. This is a non-standard semver extension used by Stardew Valley on ported platforms to represent platform-specific patches to a ported version, represented as a fourth number in the version string.</summary> public int PlatformRelease { get; } - /// <summary>An optional prerelease tag.</summary> + /// <inheritdoc /> public string PrereleaseTag { get; } - /// <summary>Optional build metadata. This is ignored when determining version precedence.</summary> + /// <inheritdoc /> public string BuildMetadata { get; } @@ -103,9 +103,7 @@ namespace StardewModdingAPI.Toolkit this.AssertValid(); } - /// <summary>Get an integer indicating whether this version precedes (less than 0), supersedes (more than 0), or is equivalent to (0) the specified version.</summary> - /// <param name="other">The version to compare with this instance.</param> - /// <exception cref="ArgumentNullException">The <paramref name="other"/> value is null.</exception> + /// <inheritdoc /> public int CompareTo(ISemanticVersion other) { if (other == null) @@ -113,68 +111,55 @@ namespace StardewModdingAPI.Toolkit return this.CompareTo(other.MajorVersion, other.MinorVersion, other.PatchVersion, (other as SemanticVersion)?.PlatformRelease ?? 0, other.PrereleaseTag); } - /// <summary>Indicates whether the current object is equal to another object of the same type.</summary> - /// <returns>true if the current object is equal to the <paramref name="other" /> parameter; otherwise, false.</returns> - /// <param name="other">An object to compare with this object.</param> + /// <inheritdoc /> public bool Equals(ISemanticVersion other) { return other != null && this.CompareTo(other) == 0; } - /// <summary>Whether this is a prerelease version.</summary> + /// <inheritdoc /> public bool IsPrerelease() { return !string.IsNullOrWhiteSpace(this.PrereleaseTag); } - /// <summary>Get whether this version is older than the specified version.</summary> - /// <param name="other">The version to compare with this instance.</param> + /// <inheritdoc /> public bool IsOlderThan(ISemanticVersion other) { return this.CompareTo(other) < 0; } - /// <summary>Get whether this version is older than the specified version.</summary> - /// <param name="other">The version to compare with this instance.</param> - /// <exception cref="FormatException">The specified version is not a valid semantic version.</exception> + /// <inheritdoc /> public bool IsOlderThan(string other) { return this.IsOlderThan(new SemanticVersion(other, allowNonStandard: true)); } - /// <summary>Get whether this version is newer than the specified version.</summary> - /// <param name="other">The version to compare with this instance.</param> + /// <inheritdoc /> public bool IsNewerThan(ISemanticVersion other) { return this.CompareTo(other) > 0; } - /// <summary>Get whether this version is newer than the specified version.</summary> - /// <param name="other">The version to compare with this instance.</param> - /// <exception cref="FormatException">The specified version is not a valid semantic version.</exception> + /// <inheritdoc /> public bool IsNewerThan(string other) { return this.IsNewerThan(new SemanticVersion(other, allowNonStandard: true)); } - /// <summary>Get whether this version is between two specified versions (inclusively).</summary> - /// <param name="min">The minimum version.</param> - /// <param name="max">The maximum version.</param> + /// <inheritdoc /> public bool IsBetween(ISemanticVersion min, ISemanticVersion max) { return this.CompareTo(min) >= 0 && this.CompareTo(max) <= 0; } - /// <summary>Get whether this version is between two specified versions (inclusively).</summary> - /// <param name="min">The minimum version.</param> - /// <param name="max">The maximum version.</param> - /// <exception cref="FormatException">One of the specified versions is not a valid semantic version.</exception> + /// <inheritdoc /> public bool IsBetween(string min, string max) { return this.IsBetween(new SemanticVersion(min, allowNonStandard: true), new SemanticVersion(max, allowNonStandard: true)); } - /// <summary>Get a string representation of the version.</summary> + /// <inheritdoc cref="ISemanticVersion.ToString" /> public override string ToString() { string version = $"{this.MajorVersion}.{this.MinorVersion}.{this.PatchVersion}"; @@ -187,7 +172,7 @@ namespace StardewModdingAPI.Toolkit return version; } - /// <summary>Whether the version uses non-standard extensions, like four-part game versions on some platforms.</summary> + /// <inheritdoc /> public bool IsNonStandard() { return this.PlatformRelease != 0; diff --git a/src/SMAPI.Toolkit/Utilities/EnvironmentUtility.cs b/src/SMAPI.Toolkit/Utilities/EnvironmentUtility.cs index 1e490448..4ef578f7 100644 --- a/src/SMAPI.Toolkit/Utilities/EnvironmentUtility.cs +++ b/src/SMAPI.Toolkit/Utilities/EnvironmentUtility.cs @@ -1,11 +1,6 @@ using System; -using System.Diagnostics; using System.Diagnostics.CodeAnalysis; -using System.Linq; -#if SMAPI_FOR_WINDOWS -using System.Management; -#endif -using System.Runtime.InteropServices; +using StardewModdingAPI.Toolkit.Framework; namespace StardewModdingAPI.Toolkit.Utilities { @@ -18,11 +13,6 @@ namespace StardewModdingAPI.Toolkit.Utilities /// <summary>The cached platform.</summary> private static Platform? CachedPlatform; - /// <summary>Get the OS name from the system uname command.</summary> - /// <param name="buffer">The buffer to fill with the resulting string.</param> - [DllImport("libc")] - static extern int uname(IntPtr buffer); - /********* ** Public methods @@ -30,7 +20,15 @@ namespace StardewModdingAPI.Toolkit.Utilities /// <summary>Detect the current OS.</summary> public static Platform DetectPlatform() { - return EnvironmentUtility.CachedPlatform ??= EnvironmentUtility.DetectPlatformImpl(); + Platform? platform = EnvironmentUtility.CachedPlatform; + + if (platform == null) + { + string rawPlatform = LowLevelEnvironmentUtility.DetectPlatform(); + EnvironmentUtility.CachedPlatform = platform = (Platform)Enum.Parse(typeof(Platform), rawPlatform, ignoreCase: true); + } + + return platform.Value; } @@ -39,132 +37,21 @@ namespace StardewModdingAPI.Toolkit.Utilities [SuppressMessage("ReSharper", "EmptyGeneralCatchClause", Justification = "Error suppressed deliberately to fallback to default behaviour.")] public static string GetFriendlyPlatformName(Platform platform) { -#if SMAPI_FOR_WINDOWS - try - { - return new ManagementObjectSearcher("SELECT Caption FROM Win32_OperatingSystem") - .Get() - .Cast<ManagementObject>() - .Select(entry => entry.GetPropertyValue("Caption").ToString()) - .FirstOrDefault(); - } - catch { } -#endif - - string name = Environment.OSVersion.ToString(); - switch (platform) - { - case Platform.Android: - name = $"Android {name}"; - break; - - case Platform.Mac: - name = $"MacOS {name}"; - break; - } - return name; + return LowLevelEnvironmentUtility.GetFriendlyPlatformName(platform.ToString()); } /// <summary>Get the name of the Stardew Valley executable.</summary> /// <param name="platform">The current platform.</param> public static string GetExecutableName(Platform platform) { - return platform == Platform.Windows - ? "Stardew Valley.exe" - : "StardewValley.exe"; + return LowLevelEnvironmentUtility.GetExecutableName(platform.ToString()); } /// <summary>Get whether the platform uses Mono.</summary> /// <param name="platform">The current platform.</param> public static bool IsMono(this Platform platform) { - return platform == Platform.Linux || platform == Platform.Mac; - } - - - /********* - ** Private methods - *********/ - /// <summary>Detect the current OS.</summary> - private static Platform DetectPlatformImpl() - { - switch (Environment.OSVersion.Platform) - { - case PlatformID.MacOSX: - return Platform.Mac; - - case PlatformID.Unix when EnvironmentUtility.IsRunningAndroid(): - return Platform.Android; - - case PlatformID.Unix when EnvironmentUtility.IsRunningMac(): - return Platform.Mac; - - case PlatformID.Unix: - return Platform.Linux; - - default: - return Platform.Windows; - } - } - - /// <summary>Detect whether the code is running on Android.</summary> - /// <remarks> - /// This code is derived from https://stackoverflow.com/a/47521647/262123. It detects Android by calling the - /// <c>getprop</c> system command to check for an Android-specific property. - /// </remarks> - private static bool IsRunningAndroid() - { - using Process process = new Process - { - StartInfo = - { - FileName = "getprop", - Arguments = "ro.build.user", - RedirectStandardOutput = true, - UseShellExecute = false, - CreateNoWindow = true - } - }; - - try - { - process.Start(); - string output = process.StandardOutput.ReadToEnd(); - return !string.IsNullOrWhiteSpace(output); - } - catch - { - return false; - } - } - - /// <summary>Detect whether the code is running on Mac.</summary> - /// <remarks> - /// This code is derived from the Mono project (see System.Windows.Forms/System.Windows.Forms/XplatUI.cs). It detects Mac by calling the - /// <c>uname</c> system command and checking the response, which is always 'Darwin' for MacOS. - /// </remarks> - private static bool IsRunningMac() - { - IntPtr buffer = IntPtr.Zero; - try - { - buffer = Marshal.AllocHGlobal(8192); - if (EnvironmentUtility.uname(buffer) == 0) - { - string os = Marshal.PtrToStringAnsi(buffer); - return os == "Darwin"; - } - return false; - } - catch - { - return false; // default to Linux - } - finally - { - if (buffer != IntPtr.Zero) - Marshal.FreeHGlobal(buffer); - } + return LowLevelEnvironmentUtility.IsMono(platform.ToString()); } } } diff --git a/src/SMAPI.Toolkit/Utilities/PathUtilities.cs b/src/SMAPI.Toolkit/Utilities/PathUtilities.cs index e9d71747..bd5fafbc 100644 --- a/src/SMAPI.Toolkit/Utilities/PathUtilities.cs +++ b/src/SMAPI.Toolkit/Utilities/PathUtilities.cs @@ -10,6 +10,13 @@ namespace StardewModdingAPI.Toolkit.Utilities public static class PathUtilities { /********* + ** Fields + *********/ + /// <summary>The root prefix for a Windows UNC path.</summary> + private const string WindowsUncRoot = @"\\"; + + + /********* ** Accessors *********/ /// <summary>The possible directory separator characters in a file path.</summary> @@ -25,6 +32,7 @@ namespace StardewModdingAPI.Toolkit.Utilities /// <summary>Get the segments from a path (e.g. <c>/usr/bin/example</c> => <c>usr</c>, <c>bin</c>, and <c>example</c>).</summary> /// <param name="path">The path to split.</param> /// <param name="limit">The number of segments to match. Any additional segments will be merged into the last returned part.</param> + [Pure] public static string[] GetSegments(string path, int? limit = null) { return limit.HasValue @@ -37,16 +45,20 @@ namespace StardewModdingAPI.Toolkit.Utilities [Pure] public static string NormalizePathSeparators(string path) { - string[] parts = PathUtilities.GetSegments(path); - string normalized = string.Join(PathUtilities.PreferredPathSeparator, parts); - if (path.StartsWith(PathUtilities.PreferredPathSeparator)) - normalized = PathUtilities.PreferredPathSeparator + normalized; // keep root slash - return normalized; + if (string.IsNullOrWhiteSpace(path)) + return path; + + return string.Join(PathUtilities.PreferredPathSeparator, path.Split(PathUtilities.PossiblePathSeparators)); } - /// <summary>Get a directory or file path relative to a given source path.</summary> + /// <summary>Get a directory or file path relative to a given source path. If no relative path is possible (e.g. the paths are on different drives), an absolute path is returned.</summary> /// <param name="sourceDir">The source folder path.</param> /// <param name="targetPath">The target folder or file path.</param> + /// <remarks> + /// + /// NOTE: this is a heuristic implementation that works in the cases SMAPI needs it for, but it doesn't handle all edge cases (e.g. case-sensitivity on Linux, or traversing between UNC paths on Windows). This should be replaced with the more comprehensive <c>Path.GetRelativePath</c> if the game ever migrates to .NET Core. + /// + /// </remarks> [Pure] public static string GetRelativePath(string sourceDir, string targetPath) { @@ -58,13 +70,27 @@ namespace StardewModdingAPI.Toolkit.Utilities // get relative path string relative = PathUtilities.NormalizePathSeparators(Uri.UnescapeDataString(from.MakeRelativeUri(to).ToString())); + + // normalize if (relative == "") - relative = "./"; + relative = "."; + else + { + // trim trailing slash from URL + if (relative.EndsWith(PathUtilities.PreferredPathSeparator)) + relative = relative.Substring(0, relative.Length - PathUtilities.PreferredPathSeparator.Length); + + // fix root + if (relative.StartsWith("file:") && !targetPath.Contains("file:")) + relative = relative.Substring("file:".Length); + } + return relative; } /// <summary>Get whether a path is relative and doesn't try to climb out of its containing folder (e.g. doesn't contain <c>../</c>).</summary> /// <param name="path">The path to check.</param> + [Pure] public static bool IsSafeRelativePath(string path) { if (string.IsNullOrWhiteSpace(path)) @@ -77,6 +103,7 @@ namespace StardewModdingAPI.Toolkit.Utilities /// <summary>Get whether a string is a valid 'slug', containing only basic characters that are safe in all contexts (e.g. filenames, URLs, etc).</summary> /// <param name="str">The string to check.</param> + [Pure] public static bool IsSlug(string str) { return !Regex.IsMatch(str, "[^a-z0-9_.-]", RegexOptions.IgnoreCase); |