Compare commits

...

10 Commits

Author SHA1 Message Date
PJB3005
cf574d7588 Version: 260.2.5 2025-12-02 00:57:04 +01:00
PJB3005
b6a4d17144 Fix NetBitArraySerializer compatibility.
Apparently NetSerializer treats IDynamicTypeSerializer and IStaticTypeSerializer differently for sealed types??

(cherry-picked from 6bbeaeeba6, without the test changes)

(cherry picked from commit d187018834dfa1cdead9ce5a7b72c38b07f8c81f)
2025-12-02 00:57:04 +01:00
PJB3005
0a47ecc5b5 Version: 260.2.4 2025-12-01 16:07:18 +01:00
PJB3005
f694f0e311 Backport BitArray .NET 10 serializer fix
83ad6042a7 & b267cd6fb4

Does not include test code to avoid risking merge conflicts.

(cherry picked from commit 415585a30d74fcae61f581808220a7aaeca3eaf5)
(cherry picked from commit e36628a6d436ea08d6d31441c101a88a5504c515)
2025-12-01 16:07:18 +01:00
PJB3005
cd90cc6e26 Version: 260.2.3 2025-09-26 13:40:44 +02:00
PJB3005
3923dd39ae Validate that content assemblies have a limited list of names.
Also, only read assemblies once from disk

(cherry picked from commit 443a8dfca65be7d60c4bd46181b4c749b4756114)
2025-09-26 13:40:44 +02:00
PJB3005
3caffa04da Version: 260.2.2 2025-09-19 09:17:28 +02:00
Skye
06b377d1d5 Fix resource loading on non-Windows platforms (#6201)
(cherry picked from commit 51bbc5dc45)
2025-09-19 09:17:28 +02:00
PJB3005
41fb191dda Version: 260.2.1 2025-09-14 14:55:52 +02:00
PJB3005
d4bcc1dc05 Squashed commit of the following:
commit d4f265c314
Author: 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.

commit 7654d38612
Author: 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...

commit cdcc255123
Author: PJB3005 <pieterjan.briers+git@gmail.com>
Date:   Sat Sep 13 19:11:16 2025 +0200

    Make Robust.Client.WebView.Cef.Program internal.

commit 2f56a6a110
Author: PJB3005 <pieterjan.briers+git@gmail.com>
Date:   Sat Sep 13 19:10:46 2025 +0200

    Update SpaceWizards.NFluidSynth to 0.2.2

commit 16fc48cef2
Author: 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)
2025-09-14 14:55:51 +02:00
22 changed files with 204 additions and 48 deletions

View File

@@ -57,7 +57,7 @@
<PackageVersion Include="SharpZstd.Interop" Version="1.5.2-beta2" />
<PackageVersion Include="SixLabors.ImageSharp" Version="3.1.7" />
<PackageVersion Include="SpaceWizards.HttpListener" Version="0.1.1" />
<PackageVersion Include="SpaceWizards.NFluidsynth" Version="0.1.1" />
<PackageVersion Include="SpaceWizards.NFluidsynth" Version="0.2.2" />
<PackageVersion Include="SpaceWizards.SharpFont" Version="1.0.2" />
<PackageVersion Include="SpaceWizards.Sodium" Version="0.2.1" />
<PackageVersion Include="TerraFX.Interop.Windows" Version="10.0.26100.1" />

View File

@@ -1,4 +1,4 @@
<Project>
<!-- This file automatically reset by Tools/version.py -->
<!-- This file automatically reset by Tools/version.py -->

View File

@@ -54,6 +54,21 @@ END TEMPLATE-->
*None yet*
## 260.2.5
## 260.2.4
## 260.2.3
## 260.2.2
## 260.2.1
## 260.2.0
### New features

View File

@@ -6,7 +6,7 @@ using Xilium.CefGlue;
namespace Robust.Client.WebView.Cef
{
public static class Program
internal static class Program
{
// This was supposed to be the main entry for the subprocess program... It doesn't work.
public static int Main(string[] args)

View File

@@ -5,6 +5,7 @@ using System.Net;
using System.Reflection;
using System.Text;
using Robust.Client.Console;
using Robust.Client.Utility;
using Robust.Shared.Configuration;
using Robust.Shared.ContentPack;
using Robust.Shared.IoC;
@@ -24,6 +25,7 @@ namespace Robust.Client.WebView.Cef
[Dependency] private readonly IDependencyCollection _dependencyCollection = default!;
[Dependency] private readonly IPrototypeManager _prototypeManager = default!;
[Dependency] private readonly IGameControllerInternal _gameController = default!;
[Dependency] private readonly IResourceManagerInternal _resourceManager = default!;
[Dependency] private readonly IClientConsoleHost _consoleHost = default!;
[Dependency] private readonly IConfigurationManager _cfg = default!;
@@ -61,7 +63,10 @@ namespace Robust.Client.WebView.Cef
var cachePath = "";
if (_resourceManager.UserData is WritableDirProvider userData)
cachePath = userData.GetFullPath(new ResPath("/cef_cache"));
{
var rootDir = UserDataDir.GetRootUserDataDir(_gameController);
cachePath = Path.Combine(rootDir, "cef_cache", "0");
}
var settings = new CefSettings()
{

View File

@@ -387,7 +387,7 @@ namespace Robust.Client
_prof.Initialize();
_resManager.Initialize(Options.LoadConfigAndUserData ? userDataDir : null);
_resManager.Initialize(Options.LoadConfigAndUserData ? userDataDir : null, hideUserDataDir: true);
var mountOptions = _commandLineArgs != null
? MountOptions.Merge(_commandLineArgs.MountOptions, Options.MountOptions)

View File

@@ -297,7 +297,7 @@ namespace Robust.Server
: null;
// Set up the VFS
_resources.Initialize(dataDir);
_resources.Initialize(dataDir, hideUserDataDir: false);
var mountOptions = _commandLineArgs != null
? MountOptions.Merge(_commandLineArgs.MountOptions, Options.MountOptions) : Options.MountOptions;

View File

@@ -6,4 +6,3 @@
[assembly: InternalsVisibleTo("Robust.Client")]
[assembly: InternalsVisibleTo("Robust.UnitTesting")]
[assembly: InternalsVisibleTo("Content.Benchmarks")]

View File

@@ -88,6 +88,7 @@ namespace Robust.Shared.ContentPack
public string SystemAssemblyName = default!;
public HashSet<VerifierError> AllowedVerifierErrors = default!;
public List<string> WhitelistedNamespaces = default!;
public List<string> AllowedAssemblyPrefixes = default!;
public Dictionary<string, Dictionary<string, TypeConfig>> Types = default!;
}

View File

@@ -131,6 +131,16 @@ namespace Robust.Shared.ContentPack
return false;
}
#pragma warning disable RA0004
var loadedConfig = _config.Result;
#pragma warning restore RA0004
if (!loadedConfig.AllowedAssemblyPrefixes.Any(allowedNamePrefix => asmName.StartsWith(allowedNamePrefix)))
{
_sawmill.Error($"Assembly name '{asmName}' is not allowed for a content assembly");
return false;
}
if (VerifyIL)
{
if (!DoVerifyIL(asmName, resolver, peReader, reader))
@@ -179,10 +189,6 @@ namespace Robust.Shared.ContentPack
return true;
}
#pragma warning disable RA0004
var loadedConfig = _config.Result;
#pragma warning restore RA0004
var badRefs = new ConcurrentBag<EntityHandle>();
// We still do explicit type reference scanning, even though the actual whitelists work with raw members.

View File

@@ -60,9 +60,7 @@ namespace Robust.Shared.ContentPack
internal string GetPath(ResPath relPath)
{
return Path.GetFullPath(Path.Combine(_directory.FullName, relPath.ToRelativeSystemPath()))
// Sanitise platform-specific path and standardize it for engine use.
.Replace(Path.DirectorySeparatorChar, '/');
return PathHelpers.SafeGetResourcePath(_directory.FullName, relPath);
}
/// <inheritdoc />

View File

@@ -14,7 +14,11 @@ namespace Robust.Shared.ContentPack
/// The directory to use for user data.
/// If null, a virtual temporary file system is used instead.
/// </param>
void Initialize(string? userData);
/// <param name="hideUserDataDir">
/// If true, <see cref="IWritableDirProvider.RootDir"/> will be hidden on
/// <see cref="IResourceManager.UserData"/>.
/// </param>
void Initialize(string? userData, bool hideUserDataDir);
/// <summary>
/// Mounts a single stream as a content file. Useful for unit testing.

View File

@@ -13,7 +13,7 @@ namespace Robust.Shared.ContentPack
{
/// <summary>
/// The root path of this provider.
/// Can be null if it's a virtual provider.
/// Can be null if it's a virtual provider or the path is protected (e.g. on the client).
/// </summary>
string? RootDir { get; }

View File

@@ -93,19 +93,23 @@ namespace Robust.Shared.ContentPack
{
var sw = Stopwatch.StartNew();
Sawmill.Debug("LOADING modules");
var files = new Dictionary<string, (ResPath Path, string[] references)>();
var files = new Dictionary<string, (ResPath Path, MemoryStream data, string[] references)>();
// Find all modules we want to load.
foreach (var fullPath in paths)
{
using var asmFile = _res.ContentFileRead(fullPath);
var refData = GetAssemblyReferenceData(asmFile);
var ms = new MemoryStream();
asmFile.CopyTo(ms);
ms.Position = 0;
var refData = GetAssemblyReferenceData(ms);
if (refData == null)
continue;
var (asmRefs, asmName) = refData.Value;
if (!files.TryAdd(asmName, (fullPath, asmRefs)))
if (!files.TryAdd(asmName, (fullPath, ms, asmRefs)))
{
Sawmill.Error("Found multiple modules with the same assembly name " +
$"'{asmName}', A: {files[asmName].Path}, B: {fullPath}.");
@@ -122,10 +126,10 @@ namespace Robust.Shared.ContentPack
Parallel.ForEach(files, pair =>
{
var (name, (path, _)) = pair;
var (name, (_, data, _)) = pair;
using var stream = _res.ContentFileRead(path);
if (!typeChecker.CheckAssembly(stream, resolver))
data.Position = 0;
if (!typeChecker.CheckAssembly(data, resolver))
{
throw new TypeCheckFailedException($"Assembly {name} failed type checks.");
}
@@ -137,14 +141,15 @@ namespace Robust.Shared.ContentPack
var nodes = TopologicalSort.FromBeforeAfter(
files,
kv => kv.Key,
kv => kv.Value.Path,
kv => kv.Value,
_ => Array.Empty<string>(),
kv => kv.Value.references,
allowMissing: true); // missing refs would be non-content assemblies so allow that.
// Actually load them in the order they depend on each other.
foreach (var path in TopologicalSort.Sort(nodes))
foreach (var item in TopologicalSort.Sort(nodes))
{
var (path, memory, _) = item;
Sawmill.Debug($"Loading module: '{path}'");
try
{
@@ -156,9 +161,9 @@ namespace Robust.Shared.ContentPack
}
else
{
using var assemblyStream = _res.ContentFileRead(path);
memory.Position = 0;
using var symbolsStream = _res.ContentFileReadOrNull(path.WithExtension("pdb"));
LoadGameAssembly(assemblyStream, symbolsStream, skipVerify: true);
LoadGameAssembly(memory, symbolsStream, skipVerify: true);
}
}
catch (Exception e)
@@ -174,7 +179,7 @@ namespace Robust.Shared.ContentPack
private (string[] refs, string name)? GetAssemblyReferenceData(Stream stream)
{
using var reader = ModLoader.MakePEReader(stream);
using var reader = ModLoader.MakePEReader(stream, leaveOpen: true);
var metaReader = reader.GetMetadataReader();
var name = metaReader.GetString(metaReader.GetAssemblyDefinition().Name);

View File

@@ -2,6 +2,7 @@
using System.Collections.Generic;
using System.IO;
using System.Runtime.InteropServices;
using Robust.Shared.Utility;
namespace Robust.Shared.ContentPack
{
@@ -63,5 +64,27 @@ namespace Robust.Shared.ContentPack
!OperatingSystem.IsWindows()
&& !OperatingSystem.IsMacOS();
internal static string SafeGetResourcePath(string baseDir, ResPath path)
{
var relSysPath = path.ToRelativeSystemPath();
if (relSysPath.Contains("\\..") || relSysPath.Contains("/.."))
{
// Hard cap on any exploit smuggling a .. in there.
// Since that could allow leaving sandbox.
throw new InvalidOperationException($"This branch should never be reached. Path: {path}");
}
var retPath = Path.GetFullPath(Path.Join(baseDir, relSysPath));
// better safe than sorry check
if (!retPath.StartsWith(baseDir))
{
// Allow path to match if it's just missing the directory separator at the end.
if (retPath != baseDir.TrimEnd(Path.DirectorySeparatorChar))
throw new InvalidOperationException($"This branch should never be reached. Path: {path}");
}
return retPath;
}
}
}

View File

@@ -41,13 +41,13 @@ namespace Robust.Shared.ContentPack
public IWritableDirProvider UserData { get; private set; } = default!;
/// <inheritdoc />
public virtual void Initialize(string? userData)
public virtual void Initialize(string? userData, bool hideRootDir)
{
Sawmill = _logManager.GetSawmill("res");
if (userData != null)
{
UserData = new WritableDirProvider(Directory.CreateDirectory(userData));
UserData = new WritableDirProvider(Directory.CreateDirectory(userData), hideRootDir);
}
else
{
@@ -379,6 +379,10 @@ namespace Robust.Shared.ContentPack
{
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);
}
}

View File

@@ -17,6 +17,10 @@ WhitelistedNamespaces:
- Content
- OpenDreamShared
AllowedAssemblyPrefixes:
- OpenDream
- Content
# The type whitelist does NOT care about which assembly types come from.
# This is because types switch assembly all the time.
# Just look up stuff like StreamReader on https://apisof.net.

View File

@@ -10,17 +10,22 @@ namespace Robust.Shared.ContentPack
/// <inheritdoc />
internal sealed class WritableDirProvider : IWritableDirProvider
{
/// <inheritdoc />
private readonly bool _hideRootDir;
public string RootDir { get; }
string? IWritableDirProvider.RootDir => _hideRootDir ? null : RootDir;
/// <summary>
/// Constructs an instance of <see cref="WritableDirProvider"/>.
/// </summary>
/// <param name="rootDir">Root file system directory to allow writing.</param>
public WritableDirProvider(DirectoryInfo rootDir)
/// <param name="hideRootDir">If true, <see cref="IWritableDirProvider.RootDir"/> is reported as null.</param>
public WritableDirProvider(DirectoryInfo rootDir, bool hideRootDir)
{
// FullName does not have a trailing separator, and we MUST have a separator.
RootDir = rootDir.FullName + Path.DirectorySeparatorChar.ToString();
_hideRootDir = hideRootDir;
}
#region File Access
@@ -119,7 +124,7 @@ namespace Robust.Shared.ContentPack
throw new FileNotFoundException();
var dirInfo = new DirectoryInfo(GetFullPath(path));
return new WritableDirProvider(dirInfo);
return new WritableDirProvider(dirInfo, _hideRootDir);
}
/// <inheritdoc />
@@ -180,20 +185,7 @@ namespace Robust.Shared.ContentPack
path = path.Clean();
return GetFullPath(RootDir, path);
}
private static string GetFullPath(string root, ResPath path)
{
var relPath = path.ToRelativeSystemPath();
if (relPath.Contains("\\..") || relPath.Contains("/.."))
{
// Hard cap on any exploit smuggling a .. in there.
// Since that could allow leaving sandbox.
throw new InvalidOperationException($"This branch should never be reached. Path: {path}");
}
return Path.GetFullPath(Path.Combine(root, relPath));
return PathHelpers.SafeGetResourcePath(RootDir, path);
}
}
}

View File

@@ -9,7 +9,6 @@
[assembly: InternalsVisibleTo("Robust.UnitTesting")]
[assembly: InternalsVisibleTo("OpenToolkit.GraphicsLibraryFramework")]
[assembly: InternalsVisibleTo("DynamicProxyGenAssembly2")] // Gives access to Castle(Moq)
[assembly: InternalsVisibleTo("Content.Benchmarks")]
[assembly: InternalsVisibleTo("Robust.Benchmarks")]
[assembly: InternalsVisibleTo("Robust.Client.WebView")]
[assembly: InternalsVisibleTo("Robust.Packaging")]

View File

@@ -0,0 +1,100 @@
using System;
using System.Collections;
using System.Collections.Generic;
using System.IO;
using System.Reflection;
using System.Reflection.Emit;
using System.Runtime.Serialization;
using JetBrains.Annotations;
using NetSerializer;
namespace Robust.Shared.Serialization;
/// <summary>
/// Custom serializer implementation for <see cref="BitArray"/>.
/// </summary>
/// <remarks>
/// <para>
/// This type is necessary as, since .NET 10, the internal layout of <see cref="BitArray"/> was changed.
/// The type now (internally) implements <see cref="ISerializable"/> for backwards compatibility with existing
/// <c>BinaryFormatter</c> code, but NetSerializer does not support <see cref="ISerializable"/>.
/// </para>
/// <para>
/// This code is designed to be backportable &amp; network compatible with the previous behavior on .NET 9.
/// </para>
/// </remarks>
internal sealed class NetBitArraySerializer : IDynamicTypeSerializer
{
// NOTE: MUST be a IDynamicTypeSerializer for compatibility!
// Can be changed in the future.
// For reference, the layout of BitArray before .NET 10 was:
// private int[] m_array;
// private int m_length;
// private int _version;
// NetSerializer serialized these in the following order (sorted by name):
// _version, m_array, m_length
public bool Handles(Type type)
{
return type == typeof(BitArray);
}
public IEnumerable<Type> GetSubtypes(Type type)
{
return [typeof(int[]), typeof(int)];
}
public void GenerateWriterMethod(Serializer serializer, Type type, ILGenerator il)
{
var method = typeof(NetBitArraySerializer).GetMethod("Write", BindingFlags.Static | BindingFlags.NonPublic)!;
// arg0: Serializer, arg1: Stream, arg2: value
il.Emit(OpCodes.Ldarg_0);
il.Emit(OpCodes.Ldarg_1);
il.Emit(OpCodes.Ldarg_2);
il.EmitCall(OpCodes.Call, method, null);
il.Emit(OpCodes.Ret);
}
public void GenerateReaderMethod(Serializer serializer, Type type, ILGenerator il)
{
var method = typeof(NetBitArraySerializer).GetMethod("Read", BindingFlags.Static | BindingFlags.NonPublic)!;
// arg0: Serializer, arg1: stream, arg2: out value
il.Emit(OpCodes.Ldarg_0);
il.Emit(OpCodes.Ldarg_1);
il.Emit(OpCodes.Ldarg_2);
il.EmitCall(OpCodes.Call, method, null);
il.Emit(OpCodes.Ret);
}
[UsedImplicitly]
private static void Write(Serializer serializer, Stream stream, BitArray value)
{
var intCount = (31 + value.Length) >> 5;
var ints = new int[intCount];
value.CopyTo(ints, 0);
serializer.SerializeDirect(stream, 0); // _version
serializer.SerializeDirect(stream, ints); // m_array
serializer.SerializeDirect(stream, value.Length); // m_length
}
[UsedImplicitly]
private static void Read(Serializer serializer, Stream stream, out BitArray value)
{
serializer.DeserializeDirect<int>(stream, out _); // _version
serializer.DeserializeDirect<int[]>(stream, out var array); // m_array
serializer.DeserializeDirect<int>(stream, out var length); // m_length
value = new BitArray(array)
{
Length = length
};
}
}

View File

@@ -91,6 +91,7 @@ namespace Robust.Shared.Serialization
MappedStringSerializer.TypeSerializer,
new Vector2Serializer(),
new Matrix3x2Serializer(),
new NetBitArraySerializer()
}
};
_serializer = new Serializer(types, settings);

View File

@@ -24,7 +24,7 @@ namespace Robust.UnitTesting.Shared.Resources
_testDir = Directory.CreateDirectory(_testDirPath);
var subDir = Path.Combine(_testDirPath, "writable");
_dirProvider = new WritableDirProvider(Directory.CreateDirectory(subDir));
_dirProvider = new WritableDirProvider(Directory.CreateDirectory(subDir), hideRootDir: false);
}
[OneTimeTearDown]