mirror of
https://github.com/space-wizards/RobustToolbox.git
synced 2026-09-15 14:52:35 +02:00
commitd4f265c314Author: PJB3005 <pieterjan.briers+git@gmail.com> Date: Sun Sep 14 14:32:44 2025 +0200 Fix incorrect path combine in DirLoader and WritableDirProvider This (and the other couple past commits) reported by Elelzedel. commit7654d38612Author: PJB3005 <pieterjan.briers+git@gmail.com> Date: Sat Sep 13 22:50:51 2025 +0200 Move CEF cache out of data directory Don't want content messing with this... commitcdcc255123Author: PJB3005 <pieterjan.briers+git@gmail.com> Date: Sat Sep 13 19:11:16 2025 +0200 Make Robust.Client.WebView.Cef.Program internal. commit2f56a6a110Author: PJB3005 <pieterjan.briers+git@gmail.com> Date: Sat Sep 13 19:10:46 2025 +0200 Update SpaceWizards.NFluidSynth to 0.2.2 commit16fc48cef2Author: PJB3005 <pieterjan.briers+git@gmail.com> Date: Sat Sep 13 19:09:43 2025 +0200 Hide IWritableDirProvider.RootDir on client This shouldn't be exposed. (cherry picked from commit 2f07159336bc640e41fbbccfdec4133a68c13bdb) (cherry picked from commit d6c3212c74373ed2420cc4be2cf10fcd899c2106) (cherry picked from commit bfa70d7e2ca6758901b680547fcfa9b24e0610b7) (cherry picked from commit 06e52f5d58efc1491915822c2650f922673c82c6)
411 lines
13 KiB
C#
411 lines
13 KiB
C#
using System;
|
|
using System.Collections.Generic;
|
|
using System.Diagnostics.CodeAnalysis;
|
|
using System.IO;
|
|
using System.Text.RegularExpressions;
|
|
using System.Threading;
|
|
using Robust.Shared.Configuration;
|
|
using Robust.Shared.IoC;
|
|
using Robust.Shared.Localization;
|
|
using Robust.Shared.Log;
|
|
using Robust.Shared.Utility;
|
|
|
|
namespace Robust.Shared.ContentPack
|
|
{
|
|
/// <summary>
|
|
/// Virtual file system for all disk resources.
|
|
/// </summary>
|
|
[Virtual]
|
|
internal partial class ResourceManager : IResourceManagerInternal
|
|
{
|
|
[Dependency] private readonly IConfigurationManager _config = default!;
|
|
[Dependency] private readonly ILogManager _logManager = default!;
|
|
|
|
private (ResPath prefix, IContentRoot root)[] _contentRoots =
|
|
new (ResPath prefix, IContentRoot root)[0];
|
|
|
|
private StreamSeekMode _streamSeekMode;
|
|
private readonly object _rootMutateLock = new();
|
|
|
|
// Special file names on Windows like serial ports.
|
|
private static readonly Regex BadPathSegmentRegex =
|
|
new("^(CON|PRN|AUX|NUL|COM[1-9]|LPT[1-9])$", RegexOptions.IgnoreCase);
|
|
|
|
// Literally not characters that can't go into filenames on Windows.
|
|
private static readonly Regex BadPathCharacterRegex =
|
|
new("[<>:\"|?*\0\\x01-\\x1f]", RegexOptions.IgnoreCase);
|
|
|
|
protected ISawmill Sawmill = default!;
|
|
|
|
/// <inheritdoc />
|
|
public IWritableDirProvider UserData { get; private set; } = default!;
|
|
|
|
/// <inheritdoc />
|
|
public virtual void Initialize(string? userData, bool hideRootDir)
|
|
{
|
|
Sawmill = _logManager.GetSawmill("res");
|
|
|
|
if (userData != null)
|
|
{
|
|
UserData = new WritableDirProvider(Directory.CreateDirectory(userData), hideRootDir);
|
|
}
|
|
else
|
|
{
|
|
UserData = new VirtualWritableDirProvider();
|
|
}
|
|
|
|
_config.OnValueChanged(CVars.ResStreamSeekMode, i => _streamSeekMode = (StreamSeekMode)i, true);
|
|
}
|
|
|
|
/// <inheritdoc />
|
|
public void MountDefaultContentPack()
|
|
{
|
|
//Assert server only
|
|
|
|
var zipPath = _config.GetCVar<string>("resource.pack");
|
|
|
|
// no pack in config
|
|
if (string.IsNullOrWhiteSpace(zipPath))
|
|
{
|
|
Sawmill.Warning("No default ContentPack to load in configuration.");
|
|
return;
|
|
}
|
|
|
|
MountContentPack(zipPath);
|
|
}
|
|
|
|
/// <inheritdoc />
|
|
public void MountContentPack(string pack, ResPath? prefix = null)
|
|
{
|
|
prefix = SanitizePrefix(prefix);
|
|
|
|
if (!Path.IsPathRooted(pack))
|
|
pack = PathHelpers.ExecutableRelativeFile(pack);
|
|
|
|
var packInfo = new FileInfo(pack);
|
|
|
|
if (!packInfo.Exists)
|
|
{
|
|
throw new FileNotFoundException("Specified ContentPack does not exist: " + packInfo.FullName);
|
|
}
|
|
|
|
//create new PackLoader
|
|
|
|
var loader = new PackLoader(packInfo, Sawmill);
|
|
AddRoot(prefix.Value, loader);
|
|
}
|
|
|
|
public void MountContentPack(Stream zipStream, ResPath? prefix = null)
|
|
{
|
|
prefix = SanitizePrefix(prefix);
|
|
|
|
var loader = new PackLoader(zipStream, Sawmill);
|
|
AddRoot(prefix.Value, loader);
|
|
}
|
|
|
|
public void AddRoot(ResPath prefix, IContentRoot loader)
|
|
{
|
|
lock (_rootMutateLock)
|
|
{
|
|
loader.Mount();
|
|
|
|
// When adding a new root we atomically swap it into the existing list.
|
|
// So the list of content roots is thread safe.
|
|
// This does make adding new roots O(n). Oh well.
|
|
var copy = _contentRoots;
|
|
Array.Resize(ref copy, copy.Length + 1);
|
|
copy[^1] = (prefix, loader);
|
|
_contentRoots = copy;
|
|
}
|
|
}
|
|
|
|
private static ResPath SanitizePrefix(ResPath? prefix)
|
|
{
|
|
if (prefix == null)
|
|
{
|
|
prefix = ResPath.Root;
|
|
}
|
|
else if (!prefix.Value.IsRooted)
|
|
{
|
|
throw new ArgumentException("Prefix must be rooted.", nameof(prefix));
|
|
}
|
|
|
|
return prefix.Value;
|
|
}
|
|
|
|
/// <inheritdoc />
|
|
public void MountContentDirectory(string path, ResPath? prefix = null)
|
|
{
|
|
prefix = SanitizePrefix(prefix);
|
|
|
|
if (!Path.IsPathRooted(path))
|
|
path = PathHelpers.ExecutableRelativeFile(path);
|
|
|
|
var pathInfo = new DirectoryInfo(path);
|
|
if (!pathInfo.Exists)
|
|
{
|
|
throw new DirectoryNotFoundException("Specified directory does not exist: " + pathInfo.FullName);
|
|
}
|
|
|
|
var loader = new DirLoader(pathInfo, _logManager.GetSawmill("res"), _config.GetCVar(CVars.ResCheckPathCasing));
|
|
AddRoot(prefix.Value, loader);
|
|
}
|
|
|
|
/// <inheritdoc />
|
|
public Stream ContentFileRead(string path)
|
|
{
|
|
return ContentFileRead(new ResPath(path));
|
|
}
|
|
|
|
/// <inheritdoc />
|
|
public Stream ContentFileRead(ResPath path)
|
|
{
|
|
if (TryContentFileRead(path, out var fileStream))
|
|
{
|
|
return fileStream;
|
|
}
|
|
|
|
throw new FileNotFoundException($"Path does not exist in the VFS: '{path}'");
|
|
}
|
|
|
|
/// <inheritdoc />
|
|
public bool TryContentFileRead(string path, [NotNullWhen(true)] out Stream? fileStream)
|
|
{
|
|
return TryContentFileRead(new ResPath(path), out fileStream);
|
|
}
|
|
|
|
/// <inheritdoc />
|
|
public bool TryContentFileRead(ResPath? path, [NotNullWhen(true)] out Stream? fileStream)
|
|
{
|
|
if (path == null)
|
|
{
|
|
throw new ArgumentNullException(nameof(path));
|
|
}
|
|
|
|
if (!path.Value.IsRooted)
|
|
{
|
|
throw new ArgumentException($"Path '{path}' must be rooted", nameof(path));
|
|
}
|
|
#if DEBUG
|
|
if (!IsPathValid(path.Value))
|
|
{
|
|
throw new FileNotFoundException($"Path '{path}' contains invalid characters/filenames.");
|
|
}
|
|
#endif
|
|
|
|
if (path.Value.CanonPath.EndsWith(ResPath.Separator))
|
|
{
|
|
// This is a folder, not a file.
|
|
fileStream = null;
|
|
return false;
|
|
}
|
|
|
|
foreach (var (prefix, root) in _contentRoots)
|
|
{
|
|
if (!path.Value.TryRelativeTo(prefix, out var relative))
|
|
{
|
|
continue;
|
|
}
|
|
|
|
if (root.TryGetFile(relative.Value, out var stream))
|
|
{
|
|
fileStream = WrapStream(stream);
|
|
return true;
|
|
}
|
|
}
|
|
|
|
fileStream = null;
|
|
return false;
|
|
}
|
|
|
|
/// <summary>
|
|
/// Apply <see cref="_streamSeekMode"/> to the provided stream.
|
|
/// </summary>
|
|
private Stream WrapStream(Stream stream)
|
|
{
|
|
switch (_streamSeekMode)
|
|
{
|
|
case StreamSeekMode.None:
|
|
return stream;
|
|
|
|
case StreamSeekMode.ForceSeekable:
|
|
if (stream.CanSeek)
|
|
return stream;
|
|
|
|
var ms = new MemoryStream(stream.CopyToArray(), writable: false);
|
|
stream.Dispose();
|
|
return ms;
|
|
|
|
case StreamSeekMode.ForceNonSeekable:
|
|
if (!stream.CanSeek)
|
|
return stream;
|
|
|
|
return new NonSeekableStream(stream);
|
|
|
|
default:
|
|
throw new InvalidOperationException();
|
|
}
|
|
}
|
|
|
|
/// <inheritdoc />
|
|
public bool ContentFileExists(string path)
|
|
{
|
|
return ContentFileExists(new ResPath(path));
|
|
}
|
|
|
|
/// <inheritdoc />
|
|
public bool ContentFileExists(ResPath path)
|
|
{
|
|
if (TryContentFileRead(path, out var stream))
|
|
{
|
|
stream.Dispose();
|
|
return true;
|
|
}
|
|
|
|
return false;
|
|
}
|
|
|
|
/// <inheritdoc />
|
|
public IEnumerable<ResPath> ContentFindFiles(string path)
|
|
{
|
|
return ContentFindFiles(new ResPath(path));
|
|
}
|
|
|
|
public IEnumerable<string> ContentGetDirectoryEntries(ResPath path)
|
|
{
|
|
if (!path.IsRooted)
|
|
throw new ArgumentException("Path is not rooted", nameof(path));
|
|
|
|
// If we don't do this, TryRelativeTo won't work correctly.
|
|
if (!path.CanonPath.EndsWith("/"))
|
|
path = new ResPath(path.CanonPath + "/");
|
|
|
|
var entries = new HashSet<string>();
|
|
|
|
foreach (var (prefix, root) in _contentRoots)
|
|
{
|
|
if (!path.TryRelativeTo(prefix, out var relative))
|
|
{
|
|
continue;
|
|
}
|
|
|
|
entries.UnionWith(root.GetEntries(relative.Value));
|
|
}
|
|
|
|
// We have to add mount points too.
|
|
// e.g. during development, /Assemblies/ is a mount point,
|
|
// and there's no explicit /Assemblies/ folder in Resources.
|
|
// So we need to manually add it since the previous pass won't catch it at all.
|
|
foreach (var (prefix, _) in _contentRoots)
|
|
{
|
|
if (!prefix.TryRelativeTo(path, out var relative))
|
|
continue;
|
|
|
|
// Return first relative segment, unless it's literally just "." (identical path).
|
|
var segments = relative.Value.EnumerateSegments();
|
|
if (segments is ["."])
|
|
continue;
|
|
|
|
entries.Add(segments[0] + "/");
|
|
}
|
|
|
|
return entries;
|
|
}
|
|
|
|
/// <inheritdoc />
|
|
public IEnumerable<ResPath> ContentFindFiles(ResPath? path)
|
|
{
|
|
if (path == null)
|
|
{
|
|
throw new ArgumentNullException(nameof(path));
|
|
}
|
|
|
|
if (!path.Value.IsRooted)
|
|
{
|
|
throw new ArgumentException("Path is not rooted", nameof(path));
|
|
}
|
|
|
|
var alreadyReturnedFiles = new HashSet<ResPath>();
|
|
|
|
foreach (var (prefix, root) in _contentRoots)
|
|
{
|
|
if (!path.Value.TryRelativeTo(prefix, out var relative))
|
|
{
|
|
continue;
|
|
}
|
|
|
|
foreach (var filename in root.FindFiles(relative.Value))
|
|
{
|
|
var newPath = prefix / filename;
|
|
if (!alreadyReturnedFiles.Contains(newPath))
|
|
{
|
|
alreadyReturnedFiles.Add(newPath);
|
|
yield return newPath;
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
public bool TryGetDiskFilePath(ResPath path, [NotNullWhen(true)] out string? diskPath)
|
|
{
|
|
// loop over each root trying to get the file
|
|
foreach (var (prefix, root) in _contentRoots)
|
|
{
|
|
if (root is not DirLoader dirLoader || !path.TryRelativeTo(prefix, out var tempPath))
|
|
{
|
|
continue;
|
|
}
|
|
|
|
diskPath = dirLoader.GetPath(tempPath.Value);
|
|
if (File.Exists(diskPath))
|
|
return true;
|
|
}
|
|
|
|
diskPath = null;
|
|
return false;
|
|
}
|
|
|
|
public void MountStreamAt(MemoryStream stream, ResPath path)
|
|
{
|
|
var loader = new SingleStreamLoader(stream, path.ToRelativePath());
|
|
AddRoot(ResPath.Root, loader);
|
|
}
|
|
|
|
public IEnumerable<ResPath> GetContentRoots()
|
|
{
|
|
foreach (var (_, root) in _contentRoots)
|
|
{
|
|
if (root is DirLoader loader)
|
|
{
|
|
var rootDir = loader.GetPath(new ResPath(@"/"));
|
|
|
|
// TODO: GET RID OF THIS.
|
|
// This code shouldn't be passing OS disk paths through ResPath.
|
|
rootDir = rootDir.Replace(Path.DirectorySeparatorChar, '/');
|
|
|
|
yield return new ResPath(rootDir);
|
|
}
|
|
}
|
|
}
|
|
|
|
internal static bool IsPathValid(ResPath path)
|
|
{
|
|
var asString = path.ToString();
|
|
if (BadPathCharacterRegex.IsMatch(asString))
|
|
{
|
|
return false;
|
|
}
|
|
|
|
foreach (var segment in path.CanonPath.Split('/'))
|
|
{
|
|
if (BadPathSegmentRegex.IsMatch(segment))
|
|
{
|
|
return false;
|
|
}
|
|
}
|
|
|
|
return true;
|
|
}
|
|
}
|
|
}
|