mirror of
https://github.com/space-wizards/RobustToolbox.git
synced 2026-02-15 03:30:53 +01:00
* RSI WiP * More work but we're doing bsdiff now * RSI loading seems to mostly work. * Vector2u deserialization test. * Add in packages again. * This is the part where I realize I need a path manipulation library. * The start of a path class but it's late so I'm going to bed. * HIGHLY theoretical ResourcePath code. Partially tested but not really. * Allow x86 for unit tests I guess jesus christ. Thanks Microsoft for still not shipping x64 VS in 2018. * Resource paths work & are tested. * I missed a doc spot. * ResourcePaths implemented on the server. * Client works with resource paths. TIME FOR A REFACTOR. * Some work but this might be a stupid idea so I migh throw it in the trash. * Resources refactored completely. They now only get the requested resourcepath. They're in charge of opening files to load. * RSI Loader WORKS. * Update AudioResource for new loading support. * Fix package references. * Fix more references. * Gonna work now?
544 lines
19 KiB
C#
544 lines
19 KiB
C#
// Because System.IO.Path sucks.
|
|
using System;
|
|
using System.Collections.Generic;
|
|
using System.Linq;
|
|
using System.Text;
|
|
|
|
namespace SS14.Shared.Utility
|
|
{
|
|
/// <summary>
|
|
/// Provides object-oriented path manipulation for resource paths.
|
|
/// ResourcePaths are immutable.
|
|
/// </summary>
|
|
public class ResourcePath
|
|
{
|
|
/// <summary>
|
|
/// The separator for the file system of the system we are compiling to.
|
|
/// Backslash on Windows, forward slash on sane systems.
|
|
/// </summary>
|
|
#if WINDOWS
|
|
public const string SYSTEM_SEPARATOR = "\\";
|
|
#else
|
|
public const string SYSTEM_SEPARATOR = "/";
|
|
#endif
|
|
|
|
/// <summary>
|
|
/// "." as a static. Separator used is <c>/</c>.
|
|
/// </summary>
|
|
public static readonly ResourcePath Self = new ResourcePath(".");
|
|
|
|
/// <summary>
|
|
/// "/" (root) as a static. Separator used is <c>/</c>.
|
|
/// </summary>
|
|
public static readonly ResourcePath Root = new ResourcePath("/");
|
|
|
|
/// <summary>
|
|
/// List of the segments of the path.
|
|
/// This is pretty much a split of the input string path by separator,
|
|
/// except for the root, which is represented as the separator in position #0.
|
|
/// </summary>
|
|
private readonly string[] Segments;
|
|
|
|
/// <summary>
|
|
/// The separator between "segments"/"directories" for this path.
|
|
/// </summary>
|
|
public string Separator { get; }
|
|
|
|
/// <summary>
|
|
/// Create a new path from a string, splitting it by the separator provided.
|
|
/// </summary>
|
|
/// <param name="path">The string path to turn into a resource path.</param>
|
|
/// <param name="separator">The separator for the resource path.</param>
|
|
/// <exception cref="ArgumentException">Thrown if you try to use "." as separator.</exception>
|
|
/// <exception cref="ArgumentNullException">Thrown if either argument is null.</exception>
|
|
public ResourcePath(string path, string separator = "/")
|
|
{
|
|
if (separator == ".")
|
|
{
|
|
throw new ArgumentException("Yeah no.", nameof(separator));
|
|
}
|
|
Separator = separator;
|
|
|
|
if (path == null)
|
|
{
|
|
throw new ArgumentNullException(nameof(path));
|
|
}
|
|
if (separator == null)
|
|
{
|
|
throw new ArgumentNullException(nameof(separator));
|
|
}
|
|
if (path == "")
|
|
{
|
|
Segments = new string[] { "." };
|
|
return;
|
|
}
|
|
|
|
var segments = new List<string>();
|
|
var splitsegments = path.Split(new string[] { separator }, StringSplitOptions.None);
|
|
var i = 0;
|
|
if (splitsegments[0] == "")
|
|
{
|
|
i = 1;
|
|
segments.Add(separator);
|
|
}
|
|
for (; i < splitsegments.Length; i++)
|
|
{
|
|
var segment = splitsegments[i];
|
|
if (segment == "" || (segment == "." && segments.Count != 0))
|
|
{
|
|
continue;
|
|
}
|
|
if (i == 1 && segments[0] == ".")
|
|
{
|
|
segments[0] = segment;
|
|
}
|
|
else
|
|
{
|
|
segments.Add(segment);
|
|
}
|
|
}
|
|
Segments = segments.ToArray();
|
|
}
|
|
|
|
private ResourcePath(string[] segments, string separator)
|
|
{
|
|
Segments = segments;
|
|
Separator = separator;
|
|
}
|
|
|
|
/// <inheritdoc />
|
|
public override string ToString()
|
|
{
|
|
var builder = new StringBuilder();
|
|
var i = 0;
|
|
if (IsRooted)
|
|
{
|
|
i = 1;
|
|
builder.Append(Separator);
|
|
}
|
|
for (; i < Segments.Length; i++)
|
|
{
|
|
builder.Append(Segments[i]);
|
|
if (i + 1 < Segments.Length)
|
|
{
|
|
builder.Append(Separator);
|
|
}
|
|
}
|
|
return builder.ToString();
|
|
}
|
|
|
|
/// <summary>
|
|
/// Returns true if the path is rooted (starts with the separator).
|
|
/// </summary>
|
|
/// <seealso cref="IsRelative" />
|
|
/// <seealso cref="ToRootedPath"/>
|
|
public bool IsRooted => Segments[0] == Separator;
|
|
|
|
/// <summary>
|
|
/// Returns true if the path is not rooted.
|
|
/// </summary>
|
|
/// <seealso cref="IsRooted" />
|
|
/// <seealso cref="ToRelativePath"/>
|
|
public bool IsRelative => !IsRooted;
|
|
|
|
/// <summary>
|
|
/// Returns true if the path is equal to "."
|
|
/// </summary>
|
|
public bool IsSelf => Segments.Length == 1 && Segments[0] == ".";
|
|
|
|
/// <summary>
|
|
/// Returns the file extension of file path, if any.
|
|
/// Returns "" if there is no file extension.
|
|
/// The extension returned does NOT include a period.
|
|
/// </summary>
|
|
public string Extension
|
|
{
|
|
get
|
|
{
|
|
var filename = Filename;
|
|
if (string.IsNullOrWhiteSpace(filename))
|
|
{
|
|
return "";
|
|
}
|
|
var index = filename.LastIndexOf('.');
|
|
if (index == 0 || index == -1 || index == filename.Length - 1)
|
|
{
|
|
// The path is a dotfile (like .bashrc),
|
|
// or there's no period at all,
|
|
// or the period is at the very end.
|
|
// Non of these cases are truly an extension.
|
|
return "";
|
|
}
|
|
return filename.Substring(index + 1);
|
|
}
|
|
}
|
|
|
|
/// <summary>
|
|
/// Returns the file name.
|
|
/// </summary>
|
|
public string Filename
|
|
{
|
|
get
|
|
{
|
|
if (Segments.Length == 1 && IsRooted)
|
|
{
|
|
return "";
|
|
}
|
|
|
|
return Segments[Segments.Length - 1];
|
|
}
|
|
}
|
|
|
|
/// <summary>
|
|
/// Returns the file name, without extension.
|
|
/// </summary>
|
|
public string FilenameWithoutExtension
|
|
{
|
|
get
|
|
{
|
|
var filename = Filename;
|
|
if (string.IsNullOrWhiteSpace(filename))
|
|
{
|
|
return filename;
|
|
}
|
|
var index = filename.LastIndexOf('.');
|
|
if (index == 0 || index == -1 || index == filename.Length - 1)
|
|
{
|
|
return filename;
|
|
}
|
|
return filename.Substring(0, index);
|
|
}
|
|
}
|
|
|
|
/// <summary>
|
|
/// Returns a new instance with a different separator set.
|
|
/// </summary>
|
|
/// <param name="newSeparator">The new separator to use.</param>
|
|
/// <exception cref="ArgumentException">Thrown if the new separator is "."</exception>
|
|
/// <exception cref="ArgumentNullException">Thrown if <paramref name="newSeparator"/> is null.</exception>
|
|
public ResourcePath ChangeSeparator(string newSeparator)
|
|
{
|
|
if (newSeparator == ".")
|
|
{
|
|
throw new ArgumentException("Yeah no.", nameof(newSeparator));
|
|
}
|
|
if (newSeparator == null)
|
|
{
|
|
throw new ArgumentNullException(nameof(newSeparator));
|
|
}
|
|
// Convert the segments into a string path, then re-parse it.
|
|
// Solves the edge case of the segments containing the new separator.
|
|
var path = new ResourcePath(Segments, newSeparator).ToString();
|
|
return new ResourcePath(path, newSeparator);
|
|
}
|
|
|
|
/// <summary>
|
|
/// Joins two resource paths together, with separator in between.
|
|
/// If the second path is absolute, the first path is completely ignored.
|
|
/// </summary>
|
|
/// <exception cref="ArgumentException">Thrown if the separators of the two paths do not match.</exception>
|
|
// "Why use / instead of +" you may think:
|
|
// * It's clever, although I got the idea from Python's pathlib.
|
|
// * It avoids confusing operator precedence causing you to join two strings,
|
|
// because path + string + string != path + (string + string),
|
|
// whereas path / (string / string) doesn't compile.
|
|
public static ResourcePath operator /(ResourcePath a, ResourcePath b)
|
|
{
|
|
if (a.Separator != b.Separator)
|
|
{
|
|
throw new ArgumentException("Both separators must be the same.");
|
|
}
|
|
if (b.IsRooted)
|
|
{
|
|
return b;
|
|
}
|
|
if (b.IsSelf)
|
|
{
|
|
return a;
|
|
}
|
|
string[] segments = new string[a.Segments.Length + b.Segments.Length];
|
|
a.Segments.CopyTo(segments, 0);
|
|
b.Segments.CopyTo(segments, a.Segments.Length);
|
|
return new ResourcePath(segments, a.Separator);
|
|
}
|
|
|
|
/// <summary>
|
|
/// Adds a new segment to the path as string.
|
|
/// </summary>
|
|
public static ResourcePath operator /(ResourcePath path, string b)
|
|
{
|
|
return path / new ResourcePath(b, path.Separator);
|
|
}
|
|
|
|
/// <summary>
|
|
/// "Cleans" the resource path, removing <c>..</c>.
|
|
/// </summary>
|
|
/// <remarks>
|
|
/// If .. appears at the base of a path, it is left alone. If it appears at root level (like /..) it is removed entirely.
|
|
/// </remarks>
|
|
public ResourcePath Clean()
|
|
{
|
|
var segments = new List<string>();
|
|
|
|
foreach (var segment in Segments)
|
|
{
|
|
// If you have ".." cleaning that up doesn't remove that.
|
|
if (segment == ".." && segments.Count != 0)
|
|
{
|
|
// Trying to do /.. results in /
|
|
if (segments.Count == 1 && segments[0] == Separator)
|
|
{
|
|
continue;
|
|
}
|
|
|
|
var pos = segments.Count - 1;
|
|
if (segments[pos] != "..")
|
|
{
|
|
segments.RemoveAt(pos);
|
|
continue;
|
|
}
|
|
}
|
|
|
|
segments.Add(segment);
|
|
}
|
|
|
|
if (segments.Count == 0)
|
|
{
|
|
return new ResourcePath(".", Separator);
|
|
}
|
|
|
|
return new ResourcePath(segments.ToArray(), Separator);
|
|
}
|
|
|
|
/// <summary>
|
|
/// Check whether a path is clean, i.e. <see cref="Clean"/> would not modify it.
|
|
/// </summary>
|
|
/// <returns></returns>
|
|
public bool IsClean()
|
|
{
|
|
for (var i = 0; i < Segments.Length; i++)
|
|
{
|
|
if (Segments[i] == "..")
|
|
{
|
|
if (IsRooted)
|
|
{
|
|
return false;
|
|
}
|
|
|
|
if (i > 0 && Segments[i - 1] != "..")
|
|
{
|
|
return false;
|
|
}
|
|
}
|
|
}
|
|
return true;
|
|
}
|
|
|
|
/// <summary>
|
|
/// Turns the path into a rooted path by prepending it with the separator.
|
|
/// Does nothing if the path is already rooted.
|
|
/// </summary>
|
|
/// <seealso cref="IsRooted" />
|
|
/// <seealso cref="ToRelativePath" />
|
|
public ResourcePath ToRootedPath()
|
|
{
|
|
if (IsRooted)
|
|
{
|
|
return this;
|
|
}
|
|
|
|
var segments = new string[Segments.Length + 1];
|
|
Segments.CopyTo(segments, 1);
|
|
segments[0] = Separator;
|
|
return new ResourcePath(segments, Separator);
|
|
}
|
|
|
|
/// <summary>
|
|
/// Turns the path into a relative path by removing the root separator, if any.
|
|
/// Does nothing if the path is already relative.
|
|
/// </summary>
|
|
/// <seealso cref="IsRelative"/>
|
|
/// <seealso cref="ToRootedPath" />
|
|
public ResourcePath ToRelativePath()
|
|
{
|
|
if (IsRelative)
|
|
{
|
|
return this;
|
|
}
|
|
|
|
var segments = new string[Segments.Length - 1];
|
|
Array.Copy(Segments, 1, segments, 0, Segments.Length - 1);
|
|
return new ResourcePath(segments, Separator);
|
|
}
|
|
|
|
/// <summary>
|
|
/// Turns the path into a relative path with system-specific separator.
|
|
/// For usage in disk I/O.
|
|
/// </summary>
|
|
public string ToRelativeSystemPath()
|
|
{
|
|
return ChangeSeparator(SYSTEM_SEPARATOR).ToRelativePath().ToString();
|
|
}
|
|
|
|
/// <summary>
|
|
/// Converts a relative disk path back into a resource path.
|
|
/// </summary>
|
|
/// <exception cref="ArgumentNullException">Thrown if either argument is null.</exception>
|
|
public static ResourcePath FromRelativeSystemPath(string path, string newseparator = "/")
|
|
{
|
|
return new ResourcePath(path, SYSTEM_SEPARATOR).ChangeSeparator(newseparator);
|
|
}
|
|
|
|
/// <summary>
|
|
/// Returns the path of how this instance is "relative" to <paramref name="basePath"/>,
|
|
/// such that <c>basePath/result == this</c>.
|
|
/// </summary>
|
|
/// <example>
|
|
/// <code>
|
|
/// var path1 = new ResourcePath("/a/b/c");
|
|
/// var path2 = new ResourcePath("/a");
|
|
/// Console.WriteLine(path1.RelativeTo(path2)); // prints "b/c".
|
|
/// </code>
|
|
/// </example>
|
|
/// <exception cref="ArgumentException">Thrown if we are not relative to the base path or the separators are not the same.</exception>
|
|
public ResourcePath RelativeTo(ResourcePath basePath)
|
|
{
|
|
if (TryRelativeTo(basePath, out var relative))
|
|
{
|
|
return relative;
|
|
}
|
|
|
|
throw new ArgumentException($"{this} does not start with {basePath}.");
|
|
}
|
|
|
|
/// <summary>
|
|
/// Try pattern version of <see cref="RelativeTo(ResourcePath)"/>
|
|
/// </summary>
|
|
/// <param name="relative">The path of how we are relative to <paramref name="basePath"/>, if at all.</param>
|
|
/// <returns>True if we are relative to <paramref name="basePath"/>, false otherwise.</returns>
|
|
/// <exception cref="ArgumentException">Thrown if the separators are not the same.</exception>
|
|
public bool TryRelativeTo(ResourcePath basePath, out ResourcePath relative)
|
|
{
|
|
if (basePath.Separator != Separator)
|
|
{
|
|
throw new ArgumentException("Separators must be the same.", nameof(basePath));
|
|
}
|
|
if (Segments.Length < basePath.Segments.Length)
|
|
{
|
|
relative = null;
|
|
return false;
|
|
}
|
|
if (Segments.Length == basePath.Segments.Length)
|
|
{
|
|
if (this == basePath)
|
|
{
|
|
relative = new ResourcePath(".", Separator);
|
|
return true;
|
|
}
|
|
else
|
|
{
|
|
relative = null;
|
|
return false;
|
|
}
|
|
}
|
|
var i = 0;
|
|
for (; i < basePath.Segments.Length; i++)
|
|
{
|
|
if (Segments[i] != basePath.Segments[i])
|
|
{
|
|
relative = null;
|
|
return false;
|
|
}
|
|
}
|
|
|
|
var segments = new string[Segments.Length - basePath.Segments.Length];
|
|
Array.Copy(Segments, basePath.Segments.Length, segments, 0, segments.Length);
|
|
relative = new ResourcePath(segments, Separator);
|
|
return true;
|
|
}
|
|
|
|
/// <summary>
|
|
/// Gets the common base of two paths.
|
|
/// </summary>
|
|
/// <example>
|
|
/// <code>
|
|
/// var path1 = new ResourcePath("/a/b/c");
|
|
/// var path2 = new ResourcePath("/a/e/d");
|
|
/// Console.WriteLine(path1.RelativeTo(path2)); // prints "/a".
|
|
/// </code>
|
|
/// </example>
|
|
/// <param name="other">The other path.</param>
|
|
/// <exception cref="ArgumentException">Thrown if there is no common base between the two paths.</exception>
|
|
public ResourcePath CommonBase(ResourcePath other)
|
|
{
|
|
if (other.Separator != Separator)
|
|
{
|
|
throw new ArgumentException("Separators must match.");
|
|
}
|
|
|
|
var i = 0;
|
|
for (; i < Segments.Length && i < other.Segments.Length; i++)
|
|
{
|
|
if (Segments[i] != other.Segments[i])
|
|
{
|
|
break;
|
|
}
|
|
}
|
|
if (i == 0)
|
|
{
|
|
throw new ArgumentException($"{this} and {other} have no common base.");
|
|
}
|
|
var segments = new string[i];
|
|
Array.Copy(Segments, segments, i);
|
|
return new ResourcePath(segments, Separator);
|
|
}
|
|
|
|
/// <inheritdoc />
|
|
public override int GetHashCode()
|
|
{
|
|
var code = Separator.GetHashCode();
|
|
foreach (var segment in Segments)
|
|
{
|
|
code |= segment.GetHashCode();
|
|
}
|
|
return code;
|
|
}
|
|
|
|
/// <inheritdoc />
|
|
public override bool Equals(object obj)
|
|
{
|
|
return obj is ResourcePath path && Equals(path);
|
|
}
|
|
|
|
/// <summary>
|
|
/// Checks that we are equal with <paramref name="path"/>.
|
|
/// This method does NOT clean the paths beforehand, so paths that point to the same location may fail if they are not cleaned beforehand.
|
|
/// Paths are never equal if they do not have the same separator.
|
|
/// </summary>
|
|
/// <param name="path">The path to check equality with.</param>
|
|
/// <returns>True if the paths are equal, false otherwise.</returns>
|
|
public bool Equals(ResourcePath path)
|
|
{
|
|
if (path == null)
|
|
{
|
|
return false;
|
|
}
|
|
return path.Separator == Separator && Segments.SequenceEqual(path.Segments);
|
|
}
|
|
|
|
public static bool operator ==(ResourcePath a, ResourcePath b)
|
|
{
|
|
if ((object)a == null)
|
|
{
|
|
return (object)b == null;
|
|
}
|
|
return a.Equals(b);
|
|
}
|
|
|
|
public static bool operator !=(ResourcePath a, ResourcePath b)
|
|
{
|
|
return !(a == b);
|
|
}
|
|
}
|
|
}
|