From 781526cf3183b34b68278c33d704fb47673890a4 Mon Sep 17 00:00:00 2001 From: Nickolas Gupton Date: Tue, 28 Nov 2023 08:57:08 -0600 Subject: [PATCH] Add "Scanning..." and "Failed" messages to downloads and misc changes (#222) * Add "Scanning..." message, add "Failed" message, clean up Codacy warnings, and set project to treat warnings as errors * Fix GoodFilesBaseUrl having an extra / * Change ApiUrls and fix merge conflicts * Split the code scanner into smaller modules where easy to do so, and fix small bug with the server view model not clearing old Downloads * Remove unused file and using statements * Comment out unused code paths, if we want these in the future we can uncomment them but for now its easier to just not include them * Split MemberReferenceScanner into smaller methods where possible * Split one more part of MemberReferenceScanner into its own method * Split MTypeReferenced.IsTypeAccessAllowed method into 2 smaller methods * Add additional logging, rename IlScanner to ILScanner, and remove unneeded Task.Run --- .vscode/extensions.json | 8 + .vscode/launch.json | 26 + .vscode/tasks.json | 41 ++ .../MockNoActiveDownloads.cs | 3 +- .../PingService/MockPingReturnsNull.cs | 2 +- .../PingService/MockPingStaticPingTime.cs | 2 +- .../PingService/MockPingThrowsException.cs | 2 +- .../Models/Api/ServerTests.cs | 1 - UnitystationLauncher.sln.DotSettings | 1 + UnitystationLauncher/Constants/ApiUrls.cs | 25 +- .../Constants/AssemblyNames.cs | 6 + UnitystationLauncher/Constants/FolderNames.cs | 8 + .../AssemblyTypeCheckerHelpers.cs | 428 +------------ .../ContentScanning/FileInfoComparer.cs | 19 - .../ContentScanning/Parsers.cs | 136 ++-- .../ContentScanning/Resolver.cs | 20 +- .../ContentScanning/Scanners/ILScanner.cs | 77 +++ .../Scanners/InheritanceScanner.cs | 53 ++ .../Scanners/MemberReferenceScanner.cs | 237 +++++++ .../Scanners/TypeAbuseScanner.cs | 31 + .../Scanners/UnmanagedMethodScanner.cs | 33 + .../ContentScanning/TypeProvider.cs | 8 +- .../Exceptions/CodeScanningException.cs | 18 + .../Exceptions/ContentLengthNullException.cs | 39 +- .../UnsupportedPlatformException.cs | 18 + .../PipeHubBuildCommunication.cs | 12 +- .../MetadataReaderExtensions.cs | 290 +++++++++ .../Infrastructure/ProgressStream.cs | 4 +- .../Infrastructure/StringExtensions.cs | 9 + .../Infrastructure/StringHandleExtensions.cs | 11 + .../Infrastructure/TypeExtensions.cs | 4 +- .../Models/Api/Changelog/BlogPost.cs | 1 - UnitystationLauncher/Models/Api/Server.cs | 4 +- .../Models/ConfigFile/SandboxConfig.cs | 9 +- .../Models/ContentScanning/MType.cs | 57 +- .../ScanningTypes/MTypeGeneric.cs | 2 +- .../ScanningTypes/MTypeReferenced.cs | 88 +++ .../ScanningTypes/MTypeSZArray.cs | 4 +- .../ScanningTypes/MTypeWackyArray.cs | 1 - .../Models/ContentScanning/TypeConfig.cs | 2 +- UnitystationLauncher/Models/Download.cs | 14 +- .../Models/Enums/DownloadState.cs | 14 + .../Services/AssemblyTypeCheckerService.cs | 585 ++---------------- UnitystationLauncher/Services/BlogService.cs | 3 - .../Services/CodeScanConfigService.cs | 195 +++--- .../Services/CodeScanService.cs | 148 ++--- .../Services/InstallationService.cs | 153 ++--- .../Interface/IAssemblyTypeCheckerService.cs | 3 +- .../Interface/ICodeScanConfigService.cs | 6 +- .../Services/Interface/ICodeScanService.cs | 2 +- .../Interface/IInstallationService.cs | 2 +- .../Services/Interface/IPingService.cs | 2 +- .../Services/Interface/IServerService.cs | 1 - UnitystationLauncher/Services/PingService.cs | 2 +- .../Services/ServerService.cs | 3 - .../UnitystationLauncher.csproj | 8 + UnitystationLauncher/ViewLocator.cs | 4 +- .../ViewModels/InstallationViewModel.cs | 1 - .../ViewModels/LauncherViewModel.cs | 2 +- .../ViewModels/MainWindowViewModel.cs | 5 - .../ViewModels/ServerViewModel.cs | 30 +- .../ViewModels/ServersPanelViewModel.cs | 2 +- UnitystationLauncher/Views/BlogPostView.axaml | 8 +- UnitystationLauncher/Views/ChangeView.axaml | 7 +- UnitystationLauncher/Views/ChangelogView.xaml | 7 +- UnitystationLauncher/Views/HubUpdateView.xaml | 8 +- .../Views/InstallationView.axaml | 8 +- .../Views/InstallationsPanelView.xaml | 9 +- UnitystationLauncher/Views/LauncherView.xaml | 7 +- UnitystationLauncher/Views/MainWindow.xaml | 206 +++--- UnitystationLauncher/Views/MainWindow.xaml.cs | 6 +- UnitystationLauncher/Views/NewsPanelView.xaml | 7 +- UnitystationLauncher/Views/PopUpDialogue.cs | 3 - UnitystationLauncher/Views/PopUpDialogue.xaml | 1 + .../Views/PreferencesPanelView.xaml | 8 +- .../Views/PreferencesPanelView.xaml.cs | 4 - UnitystationLauncher/Views/ServerView.axaml | 18 +- .../Views/ServersPanelView.xaml | 8 +- UnitystationLauncher/Views/VersionView.axaml | 8 +- 79 files changed, 1722 insertions(+), 1526 deletions(-) create mode 100644 .vscode/extensions.json create mode 100644 .vscode/launch.json create mode 100644 .vscode/tasks.json create mode 100644 UnitystationLauncher/Constants/AssemblyNames.cs create mode 100644 UnitystationLauncher/Constants/FolderNames.cs delete mode 100644 UnitystationLauncher/ContentScanning/FileInfoComparer.cs create mode 100644 UnitystationLauncher/ContentScanning/Scanners/ILScanner.cs create mode 100644 UnitystationLauncher/ContentScanning/Scanners/InheritanceScanner.cs create mode 100644 UnitystationLauncher/ContentScanning/Scanners/MemberReferenceScanner.cs create mode 100644 UnitystationLauncher/ContentScanning/Scanners/TypeAbuseScanner.cs create mode 100644 UnitystationLauncher/ContentScanning/Scanners/UnmanagedMethodScanner.cs create mode 100644 UnitystationLauncher/Exceptions/CodeScanningException.cs create mode 100644 UnitystationLauncher/Exceptions/UnsupportedPlatformException.cs create mode 100644 UnitystationLauncher/Infrastructure/MetadataReaderExtensions.cs create mode 100644 UnitystationLauncher/Infrastructure/StringExtensions.cs create mode 100644 UnitystationLauncher/Infrastructure/StringHandleExtensions.cs create mode 100644 UnitystationLauncher/Models/Enums/DownloadState.cs diff --git a/.vscode/extensions.json b/.vscode/extensions.json new file mode 100644 index 00000000..f1eb372a --- /dev/null +++ b/.vscode/extensions.json @@ -0,0 +1,8 @@ +{ + "recommendations": [ + "ms-dotnettools.csharp", + "ms-dotnettools.csdevkit", + "ms-dotnettools.vscodeintellicode-csharp", + "avaloniateam.vscode-avalonia" + ] +} \ No newline at end of file diff --git a/.vscode/launch.json b/.vscode/launch.json new file mode 100644 index 00000000..645dfaf0 --- /dev/null +++ b/.vscode/launch.json @@ -0,0 +1,26 @@ +{ + "version": "0.2.0", + "configurations": [ + { + // Use IntelliSense to find out which attributes exist for C# debugging + // Use hover for the description of the existing attributes + // For further information visit https://github.com/dotnet/vscode-csharp/blob/main/debugger-launchjson.md + "name": ".NET Core Launch (console)", + "type": "coreclr", + "request": "launch", + "preLaunchTask": "build", + // If you have changed target frameworks, make sure to update the program path. + "program": "${workspaceFolder}/UnitystationLauncher/bin/Debug/net7.0/StationHub.dll", + "args": [], + "cwd": "${workspaceFolder}/UnitystationLauncher", + // For more information about the 'console' field, see https://aka.ms/VSCode-CS-LaunchJson-Console + "console": "internalConsole", + "stopAtEntry": false + }, + { + "name": ".NET Core Attach", + "type": "coreclr", + "request": "attach" + } + ] +} \ No newline at end of file diff --git a/.vscode/tasks.json b/.vscode/tasks.json new file mode 100644 index 00000000..960a5c2d --- /dev/null +++ b/.vscode/tasks.json @@ -0,0 +1,41 @@ +{ + "version": "2.0.0", + "tasks": [ + { + "label": "build", + "command": "dotnet", + "type": "process", + "args": [ + "build", + "${workspaceFolder}/UnitystationLauncher.sln", + "/property:GenerateFullPaths=true", + "/consoleloggerparameters:NoSummary;ForceNoAlign" + ], + "problemMatcher": "$msCompile" + }, + { + "label": "publish", + "command": "dotnet", + "type": "process", + "args": [ + "publish", + "${workspaceFolder}/UnitystationLauncher.sln", + "/property:GenerateFullPaths=true", + "/consoleloggerparameters:NoSummary;ForceNoAlign" + ], + "problemMatcher": "$msCompile" + }, + { + "label": "watch", + "command": "dotnet", + "type": "process", + "args": [ + "watch", + "run", + "--project", + "${workspaceFolder}/UnitystationLauncher.sln" + ], + "problemMatcher": "$msCompile" + } + ] +} \ No newline at end of file diff --git a/UnitystationLauncher.Tests/MocksRepository/InstallationService/MockNoActiveDownloads.cs b/UnitystationLauncher.Tests/MocksRepository/InstallationService/MockNoActiveDownloads.cs index 37913dd8..8bc1b538 100644 --- a/UnitystationLauncher.Tests/MocksRepository/InstallationService/MockNoActiveDownloads.cs +++ b/UnitystationLauncher.Tests/MocksRepository/InstallationService/MockNoActiveDownloads.cs @@ -1,4 +1,5 @@ using System.Collections.Generic; +using System.Threading.Tasks; using UnitystationLauncher.Models; using UnitystationLauncher.Models.Api; using UnitystationLauncher.Services.Interface; @@ -22,7 +23,7 @@ public List GetInstallations() return null; } - public (Download?, string) DownloadInstallation(Server server) + public Task<(Download?, string)> DownloadInstallationAsync(Server server) { throw new NotImplementedException(); } diff --git a/UnitystationLauncher.Tests/MocksRepository/PingService/MockPingReturnsNull.cs b/UnitystationLauncher.Tests/MocksRepository/PingService/MockPingReturnsNull.cs index d663586b..3b78a109 100644 --- a/UnitystationLauncher.Tests/MocksRepository/PingService/MockPingReturnsNull.cs +++ b/UnitystationLauncher.Tests/MocksRepository/PingService/MockPingReturnsNull.cs @@ -6,7 +6,7 @@ namespace UnitystationLauncher.Tests.MocksRepository.PingService; public class MockPingReturnsNull : IPingService { - public Task GetPing(Server server) + public Task GetPingAsync(Server server) { return Task.FromResult(null as string)!; } diff --git a/UnitystationLauncher.Tests/MocksRepository/PingService/MockPingStaticPingTime.cs b/UnitystationLauncher.Tests/MocksRepository/PingService/MockPingStaticPingTime.cs index e53b7133..9b510e7a 100644 --- a/UnitystationLauncher.Tests/MocksRepository/PingService/MockPingStaticPingTime.cs +++ b/UnitystationLauncher.Tests/MocksRepository/PingService/MockPingStaticPingTime.cs @@ -12,7 +12,7 @@ public MockPingStaticPingTime(int pingInMilliseconds) { _pingTime = $"{pingInMilliseconds}ms"; } - public Task GetPing(Server server) + public Task GetPingAsync(Server server) { return Task.FromResult(_pingTime); } diff --git a/UnitystationLauncher.Tests/MocksRepository/PingService/MockPingThrowsException.cs b/UnitystationLauncher.Tests/MocksRepository/PingService/MockPingThrowsException.cs index 16df33c8..af3017b2 100644 --- a/UnitystationLauncher.Tests/MocksRepository/PingService/MockPingThrowsException.cs +++ b/UnitystationLauncher.Tests/MocksRepository/PingService/MockPingThrowsException.cs @@ -6,7 +6,7 @@ namespace UnitystationLauncher.Tests.MocksRepository.PingService; public class MockPingThrowsException : IPingService { - public Task GetPing(Server server) + public Task GetPingAsync(Server server) { throw new(); } diff --git a/UnitystationLauncher.Tests/Models/Api/ServerTests.cs b/UnitystationLauncher.Tests/Models/Api/ServerTests.cs index 5273427d..16bfa9f4 100644 --- a/UnitystationLauncher.Tests/Models/Api/ServerTests.cs +++ b/UnitystationLauncher.Tests/Models/Api/ServerTests.cs @@ -1,5 +1,4 @@ using UnitystationLauncher.Models.Api; -using UnitystationLauncher.Tests.MocksRepository; using UnitystationLauncher.Tests.MocksRepository.EnvironmentService; namespace UnitystationLauncher.Tests.Models.Api; diff --git a/UnitystationLauncher.sln.DotSettings b/UnitystationLauncher.sln.DotSettings index 0e6ef842..18864d1b 100644 --- a/UnitystationLauncher.sln.DotSettings +++ b/UnitystationLauncher.sln.DotSettings @@ -1,4 +1,5 @@  + IL True True True \ No newline at end of file diff --git a/UnitystationLauncher/Constants/ApiUrls.cs b/UnitystationLauncher/Constants/ApiUrls.cs index 9b9ba048..30ca5437 100644 --- a/UnitystationLauncher/Constants/ApiUrls.cs +++ b/UnitystationLauncher/Constants/ApiUrls.cs @@ -2,13 +2,20 @@ namespace UnitystationLauncher.Constants; public static class ApiUrls { - public const string ApiBaseUrl = "https://api.unitystation.org"; - public const string ServerListUrl = $"{ApiBaseUrl}/serverlist"; - public const string ValidateUrl = $"{ApiBaseUrl}/validatehubclient"; - public const string ValidateTokenUrl = $"{ApiBaseUrl}/validatetoken?data="; - public const string SignOutUrl = $"{ApiBaseUrl}/signout?data="; - - public const string ChangelogBaseUrl = "https://changelog.unitystation.org"; - public const string Latest10VersionsUrl = $"{ChangelogBaseUrl}/all-changes?format=json&limit=10"; - public const string LatestBlogPosts = $"{ChangelogBaseUrl}/posts/?format=json"; + private static string ApiBaseUrl => "https://api.unitystation.org"; + public static string ServerListUrl => $"{ApiBaseUrl}/serverlist"; + public static string ValidateUrl => $"{ApiBaseUrl}/validatehubclient"; + public static string ValidateTokenUrl => $"{ApiBaseUrl}/validatetoken?data="; + public static string SignOutUrl => $"{ApiBaseUrl}/signout?data="; + + private static string ChangelogBaseUrl => "https://changelog.unitystation.org"; + public static string Latest10VersionsUrl => $"{ChangelogBaseUrl}/all-changes?format=json&limit=10"; + public static string LatestBlogPosts => $"{ChangelogBaseUrl}/posts/?format=json"; + + private static string CdnBaseUrl => "https://unitystationfile.b-cdn.net"; + public static string GoodFilesBaseUrl => $"{CdnBaseUrl}/GoodFiles"; + public static string AllowedGoodFilesUrl => $"{GoodFilesBaseUrl}/AllowGoodFiles.json"; + + private static string RawGitHubFileBaseUrl => "https://raw.githubusercontent.com/unitystation/unitystation/develop"; + public static string CodeScanListUrl => $"{RawGitHubFileBaseUrl}/CodeScanList.json"; } \ No newline at end of file diff --git a/UnitystationLauncher/Constants/AssemblyNames.cs b/UnitystationLauncher/Constants/AssemblyNames.cs new file mode 100644 index 00000000..c4f30917 --- /dev/null +++ b/UnitystationLauncher/Constants/AssemblyNames.cs @@ -0,0 +1,6 @@ +namespace UnitystationLauncher.Constants; + +public static class AssemblyNames +{ + public const string SystemAssemblyName = "mscorlib"; +} \ No newline at end of file diff --git a/UnitystationLauncher/Constants/FolderNames.cs b/UnitystationLauncher/Constants/FolderNames.cs new file mode 100644 index 00000000..8b2720c1 --- /dev/null +++ b/UnitystationLauncher/Constants/FolderNames.cs @@ -0,0 +1,8 @@ +namespace UnitystationLauncher.Constants; + +public static class FolderNames +{ + public const string Managed = "Managed"; + public const string Plugins = "Plugins"; + public const string UnitystationData = "Unitystation_Data"; +} \ No newline at end of file diff --git a/UnitystationLauncher/ContentScanning/AssemblyTypeCheckerHelpers.cs b/UnitystationLauncher/ContentScanning/AssemblyTypeCheckerHelpers.cs index c7689f77..7db1a8f5 100644 --- a/UnitystationLauncher/ContentScanning/AssemblyTypeCheckerHelpers.cs +++ b/UnitystationLauncher/ContentScanning/AssemblyTypeCheckerHelpers.cs @@ -1,13 +1,9 @@ using System; -using System.Collections.Concurrent; -using System.Collections.Generic; -using System.Diagnostics.CodeAnalysis; using System.IO; -using System.Linq; -using System.Reflection; using System.Reflection.Metadata; -using UnitystationLauncher.Exceptions; -using UnitystationLauncher.Models.ContentScanning; +using ILVerify; +using UnitystationLauncher.Infrastructure; +using UnitystationLauncher.Models.ConfigFile; using UnitystationLauncher.Models.ContentScanning.ScanningTypes; // psst @@ -24,424 +20,38 @@ namespace UnitystationLauncher.ContentScanning; /// internal static class AssemblyTypeCheckerHelpers { - // Used to be in Sandbox.yml, moved out of there to facilitate faster loading. - internal const string SystemAssemblyName = "mscorlib"; //TODO check security - //UnityEngine.dll - //mscorlib - //System.Runtime - - internal static Resolver CreateResolver(DirectoryInfo ManagedPath) - { - return new Resolver(ManagedPath); - } - - internal static string FormatMethodName(MetadataReader reader, MethodDefinition method) - { - MethodSignature methodSig = method.DecodeSignature(new TypeProvider(), 0); - MTypeDefined type = GetTypeFromDefinition(reader, method.GetDeclaringType()); - - return - $"{type}.{reader.GetString(method.Name)}({string.Join(", ", methodSig.ParameterTypes)}) Returns {methodSig.ReturnType} "; - } - - internal static void CheckNoUnmanagedMethodDefs(MetadataReader reader, ConcurrentBag errors) - { - foreach (MethodDefinitionHandle methodDefHandle in reader.MethodDefinitions) - { - MethodDefinition methodDef = reader.GetMethodDefinition(methodDefHandle); - MethodImplAttributes implAttr = methodDef.ImplAttributes; - MethodAttributes attr = methodDef.Attributes; - - if ((implAttr & MethodImplAttributes.Unmanaged) != 0 || - (implAttr & MethodImplAttributes.CodeTypeMask) is not (MethodImplAttributes.IL - or MethodImplAttributes.Runtime)) - { - string err = $"Method has illegal MethodImplAttributes: {FormatMethodName(reader, methodDef)}"; - errors.Add(new SandboxError(err)); - } - - if ((attr & (MethodAttributes.PinvokeImpl | MethodAttributes.UnmanagedExport)) != 0) - { - string err = $"Method has illegal MethodAttributes: {FormatMethodName(reader, methodDef)}"; - errors.Add(new SandboxError(err)); - } - } - } - - internal static void CheckNoTypeAbuse(MetadataReader reader, ConcurrentBag errors) - { - foreach (TypeDefinitionHandle typeDefHandle in reader.TypeDefinitions) - { - TypeDefinition typeDef = reader.GetTypeDefinition(typeDefHandle); - if ((typeDef.Attributes & TypeAttributes.ExplicitLayout) != 0) - { - // The C# compiler emits explicit layout types for some array init logic. These have no fields. - // Only ban explicit layout if it has fields. - - MTypeDefined type = GetTypeFromDefinition(reader, typeDefHandle); - - if (typeDef.GetFields().Count > 0) - { - string err = $"Explicit layout type {type} may not have fields."; - errors.Add(new SandboxError(err)); - } - } - } - } - - internal static List GetReferencedTypes(MetadataReader reader, ConcurrentBag errors) + internal static Resolver CreateResolver(DirectoryInfo managedPath) { - return reader.TypeReferences.Select(typeRefHandle => - { - try - { - return ParseTypeReference(reader, typeRefHandle); - } - catch (UnsupportedMetadataException e) - { - errors.Add(new SandboxError(e)); - return null; - } - }) - .Where(p => p != null) - .ToList()!; + return new(managedPath); } - internal static List GetReferencedMembers(MetadataReader reader, ConcurrentBag errors) + internal static bool CheckVerificationResult(SandboxConfig loadedCfg, VerificationResult res, string name, MetadataReader reader, Action logErrors) { - bool Parallel = true; - if (Parallel) - { - return reader.MemberReferences.AsParallel() - .Select(memRefHandle => - { - MemberReference memRef = reader.GetMemberReference(memRefHandle); - string memName = reader.GetString(memRef.Name); - MType parent; - switch (memRef.Parent.Kind) - { - // See II.22.25 in ECMA-335. - case HandleKind.TypeReference: - { - // Regular type reference. - try - { - parent = ParseTypeReference(reader, (TypeReferenceHandle)memRef.Parent); - } - catch (UnsupportedMetadataException u) - { - errors.Add(new SandboxError(u)); - return null; - } - - break; - } - case HandleKind.TypeDefinition: - { - try - { - parent = GetTypeFromDefinition(reader, (TypeDefinitionHandle)memRef.Parent); - } - catch (UnsupportedMetadataException u) - { - errors.Add(new SandboxError(u)); - return null; - } - - break; - } - case HandleKind.TypeSpecification: - { - TypeSpecification typeSpec = reader.GetTypeSpecification((TypeSpecificationHandle)memRef.Parent); - // Generic type reference. - TypeProvider provider = new TypeProvider(); - parent = typeSpec.DecodeSignature(provider, 0); - - if (parent.IsCoreTypeDefined()) - { - // Ensure this isn't a self-defined type. - // This can happen due to generics since MethodSpec needs to point to MemberRef. - return null; - } - - break; - } - case HandleKind.ModuleReference: - { - errors.Add(new SandboxError( - $"Module global variables and methods are unsupported. Name: {memName}")); - return null; - } - case HandleKind.MethodDefinition: - { - errors.Add(new SandboxError($"Vararg calls are unsupported. Name: {memName}")); - return null; - } - default: - { - errors.Add(new SandboxError( - $"Unsupported member ref parent type: {memRef.Parent.Kind}. Name: {memName}")); - return null; - } - } - - MMemberRef memberRef; - - switch (memRef.GetKind()) - { - case MemberReferenceKind.Method: - { - MethodSignature sig = memRef.DecodeMethodSignature(new TypeProvider(), 0); - - memberRef = new MMemberRefMethod( - parent, - memName, - sig.ReturnType, - sig.GenericParameterCount, - sig.ParameterTypes); - - break; - } - case MemberReferenceKind.Field: - { - MType fieldType = memRef.DecodeFieldSignature(new TypeProvider(), 0); - memberRef = new MMemberRefField(parent, memName, fieldType); - break; - } - default: - throw new ArgumentOutOfRangeException(); - } - - return memberRef; - }) - .Where(p => p != null) - .ToList()!; - } - else + if (loadedCfg.AllowedVerifierErrors.Contains(res.Code)) { - return reader.MemberReferences.Select(memRefHandle => - { - MemberReference memRef = reader.GetMemberReference(memRefHandle); - string memName = reader.GetString(memRef.Name); - MType parent; - switch (memRef.Parent.Kind) - { - // See II.22.25 in ECMA-335. - case HandleKind.TypeReference: - { - // Regular type reference. - try - { - parent = ParseTypeReference(reader, (TypeReferenceHandle)memRef.Parent); - } - catch (UnsupportedMetadataException u) - { - errors.Add(new SandboxError(u)); - return null; - } - - break; - } - case HandleKind.TypeDefinition: - { - try - { - parent = GetTypeFromDefinition(reader, (TypeDefinitionHandle)memRef.Parent); - } - catch (UnsupportedMetadataException u) - { - errors.Add(new SandboxError(u)); - return null; - } - - break; - } - case HandleKind.TypeSpecification: - { - TypeSpecification typeSpec = reader.GetTypeSpecification((TypeSpecificationHandle)memRef.Parent); - // Generic type reference. - TypeProvider provider = new TypeProvider(); - parent = typeSpec.DecodeSignature(provider, 0); - - if (parent.IsCoreTypeDefined()) - { - // Ensure this isn't a self-defined type. - // This can happen due to generics since MethodSpec needs to point to MemberRef. - return null; - } - - break; - } - case HandleKind.ModuleReference: - { - errors.Add(new SandboxError( - $"Module global variables and methods are unsupported. Name: {memName}")); - return null; - } - case HandleKind.MethodDefinition: - { - errors.Add(new SandboxError($"Vararg calls are unsupported. Name: {memName}")); - return null; - } - default: - { - errors.Add(new SandboxError( - $"Unsupported member ref parent type: {memRef.Parent.Kind}. Name: {memName}")); - return null; - } - } - - MMemberRef memberRef; - - switch (memRef.GetKind()) - { - case MemberReferenceKind.Method: - { - MethodSignature sig = memRef.DecodeMethodSignature(new TypeProvider(), 0); - - memberRef = new MMemberRefMethod( - parent, - memName, - sig.ReturnType, - sig.GenericParameterCount, - sig.ParameterTypes); - - break; - } - case MemberReferenceKind.Field: - { - MType fieldType = memRef.DecodeFieldSignature(new TypeProvider(), 0); - memberRef = new MMemberRefField(parent, memName, fieldType); - break; - } - default: - throw new ArgumentOutOfRangeException(); - } - - return memberRef; - }) - .Where(p => p != null) - .ToList()!; + return false; } - } - internal static bool ParseInheritType(MType ownerType, EntityHandle handle, [NotNullWhen(true)] out MType? type, MetadataReader reader, ConcurrentBag errors) - { - type = default; + string formatted = res.Args == null ? res.Message : string.Format(res.Message, res.Args); + string msg = $"{name}: ILVerify: {formatted}"; - switch (handle.Kind) + if (!res.Method.IsNil) { - case HandleKind.TypeDefinition: - // Definition to type in same assembly, allowed without hassle. - return false; - - case HandleKind.TypeReference: - // Regular type reference. - try - { - type = ParseTypeReference(reader, (TypeReferenceHandle)handle); - return true; - } - catch (UnsupportedMetadataException u) - { - errors.Add(new SandboxError(u)); - return false; - } - - case HandleKind.TypeSpecification: - TypeSpecification typeSpec = reader.GetTypeSpecification((TypeSpecificationHandle)handle); - // Generic type reference. - TypeProvider provider = new TypeProvider(); - type = typeSpec.DecodeSignature(provider, 0); - - if (type.IsCoreTypeDefined()) - { - // Ensure this isn't a self-defined type. - // This can happen due to generics. - return false; - } - - break; + MethodDefinition method = reader.GetMethodDefinition(res.Method); + string methodName = reader.FormatMethodName(method); - default: - errors.Add(new SandboxError( - $"Unsupported BaseType of kind {handle.Kind} on type {ownerType}")); - return false; + msg = $"{msg}, method: {methodName}"; } - type = default!; - return false; - } - - /// - /// Thrown if the metadata does something funny we don't "support" like type forwarding. - /// - internal static MTypeReferenced ParseTypeReference(MetadataReader reader, TypeReferenceHandle handle) - { - TypeReference typeRef = reader.GetTypeReference(handle); - string name = reader.GetString(typeRef.Name); - string? nameSpace = NilNullString(reader, typeRef.Namespace); - MResScope resScope; - - // See II.22.38 in ECMA-335 - if (typeRef.ResolutionScope.IsNil) + if (!res.Type.IsNil) { - throw new UnsupportedMetadataException( - $"Null resolution scope on type Name: {nameSpace}.{name}. This indicates exported/forwarded types"); + MTypeDefined type = reader.GetTypeFromDefinition(res.Type); + msg = $"{msg}, type: {type}"; } - switch (typeRef.ResolutionScope.Kind) - { - case HandleKind.AssemblyReference: - { - // Different assembly. - AssemblyReference assemblyRef = - reader.GetAssemblyReference((AssemblyReferenceHandle)typeRef.ResolutionScope); - string assemblyName = reader.GetString(assemblyRef.Name); - resScope = new MResScopeAssembly(assemblyName); - break; - } - case HandleKind.TypeReference: - { - // Nested type. - MTypeReferenced enclosingType = ParseTypeReference(reader, (TypeReferenceHandle)typeRef.ResolutionScope); - resScope = new MResScopeType(enclosingType); - break; - } - case HandleKind.ModuleReference: - { - // Same-assembly-different-module - throw new UnsupportedMetadataException( - $"Cross-module reference to type {nameSpace}.{name}. "); - } - default: - // Edge cases not handled: - // https://github.com/dotnet/runtime/blob/b2e5a89085fcd87e2fa9300b4bb00cd499c5845b/src/libraries/System.Reflection.Metadata/tests/Metadata/Decoding/DisassemblingTypeProvider.cs#L130-L132 - throw new UnsupportedMetadataException( - $"TypeRef to {typeRef.ResolutionScope.Kind} for type {nameSpace}.{name}"); - } - - return new MTypeReferenced(resScope, name, nameSpace); - } - - internal static string? NilNullString(MetadataReader reader, StringHandle handle) - { - return handle.IsNil ? null : reader.GetString(handle); + logErrors.Invoke(msg); + return true; } - - internal static MTypeDefined GetTypeFromDefinition(MetadataReader reader, TypeDefinitionHandle handle) - { - TypeDefinition typeDef = reader.GetTypeDefinition(handle); - string name = reader.GetString(typeDef.Name); - string? ns = NilNullString(reader, typeDef.Namespace); - MTypeDefined? enclosing = null; - if (typeDef.IsNested) - { - enclosing = GetTypeFromDefinition(reader, typeDef.GetDeclaringType()); - } - - return new MTypeDefined(name, ns, enclosing); - } } \ No newline at end of file diff --git a/UnitystationLauncher/ContentScanning/FileInfoComparer.cs b/UnitystationLauncher/ContentScanning/FileInfoComparer.cs deleted file mode 100644 index 0b704464..00000000 --- a/UnitystationLauncher/ContentScanning/FileInfoComparer.cs +++ /dev/null @@ -1,19 +0,0 @@ -using System; -using System.Collections.Generic; -using System.IO; - -namespace UnitystationLauncher.ContentScanning; - -internal class FileInfoComparer : IEqualityComparer -{ - public bool Equals(FileInfo? x, FileInfo? y) - { - if (x == null || y == null) return false; - return x.Name.Equals(y.Name, StringComparison.OrdinalIgnoreCase); - } - - public int GetHashCode(FileInfo obj) - { - return obj.Name.GetHashCode(); - } -} \ No newline at end of file diff --git a/UnitystationLauncher/ContentScanning/Parsers.cs b/UnitystationLauncher/ContentScanning/Parsers.cs index d68f7cf3..86f192ed 100644 --- a/UnitystationLauncher/ContentScanning/Parsers.cs +++ b/UnitystationLauncher/ContentScanning/Parsers.cs @@ -15,114 +15,114 @@ public static class Parsers { // Contains primary parsing code for method and field declarations in the sandbox whitelist. - private static readonly Parser VoidTypeParser = + private static readonly Parser _voidTypeParser = String("void").ThenReturn(PrimitiveTypeCode.Void); - private static readonly Parser BooleanTypeParser = + private static readonly Parser _booleanTypeParser = String("bool").ThenReturn(PrimitiveTypeCode.Boolean); - private static readonly Parser CharTypeParser = + private static readonly Parser _charTypeParser = String("char").ThenReturn(PrimitiveTypeCode.Char); - private static readonly Parser SByteTypeParser = + private static readonly Parser _sByteTypeParser = String("sbyte").ThenReturn(PrimitiveTypeCode.SByte); - private static readonly Parser ByteTypeParser = + private static readonly Parser _byteTypeParser = String("byte").ThenReturn(PrimitiveTypeCode.Byte); - private static readonly Parser Int16TypeParser = + private static readonly Parser _int16TypeParser = String("short").ThenReturn(PrimitiveTypeCode.Int16); - private static readonly Parser UInt16TypeParser = + private static readonly Parser _uInt16TypeParser = String("ushort").ThenReturn(PrimitiveTypeCode.UInt16); - private static readonly Parser Int32TypeParser = + private static readonly Parser _int32TypeParser = String("int").ThenReturn(PrimitiveTypeCode.Int32); - private static readonly Parser UInt32TypeParser = + private static readonly Parser _uInt32TypeParser = String("uint").ThenReturn(PrimitiveTypeCode.UInt32); - private static readonly Parser Int64TypeParser = + private static readonly Parser _int64TypeParser = String("long").ThenReturn(PrimitiveTypeCode.Int64); - private static readonly Parser UInt64TypeParser = + private static readonly Parser _uInt64TypeParser = String("ulong").ThenReturn(PrimitiveTypeCode.UInt64); - private static readonly Parser IntPtrTypeParser = + private static readonly Parser _intPtrTypeParser = String("nint").ThenReturn(PrimitiveTypeCode.IntPtr); - private static readonly Parser UIntPtrTypeParser = + private static readonly Parser _uIntPtrTypeParser = String("nuint").ThenReturn(PrimitiveTypeCode.UIntPtr); - private static readonly Parser SingleTypeParser = + private static readonly Parser _singleTypeParser = String("float").ThenReturn(PrimitiveTypeCode.Single); - private static readonly Parser DoubleTypeParser = + private static readonly Parser _doubleTypeParser = String("double").ThenReturn(PrimitiveTypeCode.Double); - private static readonly Parser StringTypeParser = + private static readonly Parser _stringTypeParser = String("string").ThenReturn(PrimitiveTypeCode.String); - private static readonly Parser ObjectTypeParser = + private static readonly Parser _objectTypeParser = String("object").ThenReturn(PrimitiveTypeCode.Object); - private static readonly Parser TypedReferenceTypeParser = + private static readonly Parser _typedReferenceTypeParser = String("typedref").ThenReturn(PrimitiveTypeCode.TypedReference); - private static readonly Parser PrimitiveTypeParser = + private static readonly Parser _primitiveTypeParser = OneOf( - Try(VoidTypeParser), - Try(BooleanTypeParser), - Try(CharTypeParser), - Try(SByteTypeParser), - Try(ByteTypeParser), - Try(Int16TypeParser), - Try(UInt16TypeParser), - Try(Int32TypeParser), - Try(UInt32TypeParser), - Try(Int64TypeParser), - Try(UInt64TypeParser), - Try(IntPtrTypeParser), - Try(UIntPtrTypeParser), - Try(SingleTypeParser), - Try(DoubleTypeParser), - Try(StringTypeParser), - Try(ObjectTypeParser), - TypedReferenceTypeParser) + Try(_voidTypeParser), + Try(_booleanTypeParser), + Try(_charTypeParser), + Try(_sByteTypeParser), + Try(_byteTypeParser), + Try(_int16TypeParser), + Try(_uInt16TypeParser), + Try(_int32TypeParser), + Try(_uInt32TypeParser), + Try(_int64TypeParser), + Try(_uInt64TypeParser), + Try(_intPtrTypeParser), + Try(_uIntPtrTypeParser), + Try(_singleTypeParser), + Try(_doubleTypeParser), + Try(_stringTypeParser), + Try(_objectTypeParser), + _typedReferenceTypeParser) .Select(code => (MType)new MTypePrimitive(code)).Labelled("Primitive type"); - private static readonly Parser NamespacedIdentifier = + private static readonly Parser _namespacedIdentifier = Token(c => char.IsLetterOrDigit(c) || c == '.' || c == '_' || c == '`') .AtLeastOnceString() .Labelled("valid identifier"); - private static readonly Parser> GenericParametersParser = - Rec(() => MaybeArrayTypeParser!) + private static readonly Parser> _genericParametersParser = + Rec(() => _maybeArrayTypeParser!) .Between(SkipWhitespaces) .Separated(Char(',')) .Between(Char('<'), Char('>')); - private static readonly Parser GenericMethodPlaceholderParser = + private static readonly Parser _genericMethodPlaceholderParser = String("!!") .Then(Digit.AtLeastOnceString()) .Select(p => (MType)new MTypeGenericMethodPlaceHolder(int.Parse(p, CultureInfo.InvariantCulture))); - private static readonly Parser GenericTypePlaceholderParser = + private static readonly Parser _genericTypePlaceholderParser = String("!") .Then(Digit.AtLeastOnceString()) .Select(p => (MType)new MTypeGenericTypePlaceHolder(int.Parse(p, CultureInfo.InvariantCulture))); - private static readonly Parser GenericPlaceholderParser = Try(GenericTypePlaceholderParser) - .Or(Try(GenericMethodPlaceholderParser)).Labelled("Generic placeholder"); + private static readonly Parser _genericPlaceholderParser = Try(_genericTypePlaceholderParser) + .Or(Try(_genericMethodPlaceholderParser)).Labelled("Generic placeholder"); - private static readonly Parser TypeNameParser = - Parser.Map( - (a, b) => b.Aggregate(new MTypeParsed(a), (parsed, s) => new MTypeParsed(s, parsed)), - NamespacedIdentifier, - Char('/').Then(NamespacedIdentifier).Many()); + private static readonly Parser _typeNameParser = + Map( + (a, b) => b.Aggregate(new MTypeParsed(a), (parsed, s) => new(s, parsed)), + _namespacedIdentifier, + Char('/').Then(_namespacedIdentifier).Many()); - private static readonly Parser ConstructedObjectTypeParser = - Parser.Map((arg1, arg2) => + private static readonly Parser _constructedObjectTypeParser = + Map((arg1, arg2) => { MType type = arg1; if (arg2.HasValue) @@ -132,25 +132,25 @@ public static class Parsers return type; }, - TypeNameParser, - GenericParametersParser.Optional()); + _typeNameParser, + _genericParametersParser.Optional()); - private static readonly Parser MaybeArrayTypeParser = Parser.Map( - (a, b) => b.Aggregate(a, (type, _) => new MTypeSZArray(type)), - Try(GenericPlaceholderParser).Or(Try(PrimitiveTypeParser)).Or(ConstructedObjectTypeParser), + private static readonly Parser _maybeArrayTypeParser = Map( + (a, b) => b.Aggregate(a, (type, _) => new MTypeSzArray(type)), + Try(_genericPlaceholderParser).Or(Try(_primitiveTypeParser)).Or(_constructedObjectTypeParser), String("[]").Many()); - private static readonly Parser ByRefTypeParser = + private static readonly Parser _byRefTypeParser = String("ref") .Then(SkipWhitespaces) - .Then(MaybeArrayTypeParser) + .Then(_maybeArrayTypeParser) .Select(t => (MType)new MTypeByRef(t)) .Labelled("ByRef type"); - private static readonly Parser TypeParser = Try(ByRefTypeParser).Or(MaybeArrayTypeParser); + private static readonly Parser _typeParser = Try(_byRefTypeParser).Or(_maybeArrayTypeParser); - private static readonly Parser> MethodParamsParser = - TypeParser + private static readonly Parser> _methodParamsParser = + _typeParser .Between(SkipWhitespaces) .Separated(Char(',')) .Between(Char('('), Char(')')) @@ -160,15 +160,15 @@ public static class Parsers Try(Char(',').Many().Select(p => p.Count() + 1).Between(Char('<'), Char('>'))).Or(Return(0)); internal static readonly Parser MethodParser = - Parser.Map( + Map( (a, b, d, c) => new WhitelistMethodDefine(b, a, c.ToList(), d), - SkipWhitespaces.Then(TypeParser), - SkipWhitespaces.Then(NamespacedIdentifier), + SkipWhitespaces.Then(_typeParser), + SkipWhitespaces.Then(_namespacedIdentifier), MethodGenericParameterCountParser, - SkipWhitespaces.Then(MethodParamsParser)); + SkipWhitespaces.Then(_methodParamsParser)); - internal static readonly Parser FieldParser = Parser.Map( + internal static readonly Parser FieldParser = Map( (a, b) => new WhitelistFieldDefine(b, a), - MaybeArrayTypeParser.Between(SkipWhitespaces), - NamespacedIdentifier); + _maybeArrayTypeParser.Between(SkipWhitespaces), + _namespacedIdentifier); } \ No newline at end of file diff --git a/UnitystationLauncher/ContentScanning/Resolver.cs b/UnitystationLauncher/ContentScanning/Resolver.cs index b41dfe4f..ec682ca6 100644 --- a/UnitystationLauncher/ContentScanning/Resolver.cs +++ b/UnitystationLauncher/ContentScanning/Resolver.cs @@ -1,10 +1,10 @@ - using System; using System.Collections.Generic; using System.IO; using System.Reflection; using System.Reflection.PortableExecutable; using ILVerify; +using Serilog; namespace UnitystationLauncher.ContentScanning; @@ -13,7 +13,7 @@ public sealed class Resolver : IResolver private readonly DirectoryInfo _managedPath; - private readonly Dictionary _dictionaryLookup = new Dictionary(); + private readonly Dictionary _dictionaryLookup = new(); public Resolver(DirectoryInfo inManagedPath) { @@ -22,9 +22,9 @@ public Resolver(DirectoryInfo inManagedPath) public void Dispose() { - foreach (KeyValuePair Lookup in _dictionaryLookup) + foreach (KeyValuePair lookup in _dictionaryLookup) { - Lookup.Value.Dispose(); + lookup.Value.Dispose(); } } @@ -47,9 +47,9 @@ PEReader IResolver.ResolveAssembly(AssemblyName assemblyName) string fileName = Path.GetFileNameWithoutExtension(file.Name); if (string.Equals(fileName, assemblyName.Name, StringComparison.OrdinalIgnoreCase)) { - Console.WriteLine($"Found DLL for assembly '{assemblyName.Name}': {file.FullName}"); + Log.Information($"Found DLL for assembly '{assemblyName.Name}': {file.FullName}"); _dictionaryLookup[assemblyName.Name] = - new PEReader(file.Open(FileMode.Open, FileAccess.Read, FileShare.Read)); + new(file.Open(FileMode.Open, FileAccess.Read, FileShare.Read)); return _dictionaryLookup[assemblyName.Name]; } } @@ -61,9 +61,9 @@ PEReader IResolver.ResolveAssembly(AssemblyName assemblyName) string fileName = Path.GetFileNameWithoutExtension(file.Name); if (string.Equals(fileName, assemblyName.Name, StringComparison.OrdinalIgnoreCase)) { - Console.WriteLine($"Found DLL for assembly '{assemblyName.Name}': {file.FullName}"); + Log.Information($"Found DLL for assembly '{assemblyName.Name}': {file.FullName}"); _dictionaryLookup[assemblyName.Name] = - new PEReader(file.Open(FileMode.Open, FileAccess.Read, FileShare.Read)); + new(file.Open(FileMode.Open, FileAccess.Read, FileShare.Read)); return _dictionaryLookup[assemblyName.Name]; } } @@ -75,9 +75,9 @@ PEReader IResolver.ResolveAssembly(AssemblyName assemblyName) string fileName = Path.GetFileNameWithoutExtension(file.Name); if (string.Equals(fileName, assemblyName.Name, StringComparison.OrdinalIgnoreCase)) { - Console.WriteLine($"Found DLL for assembly '{assemblyName.Name}': {file.FullName}"); + Log.Information($"Found DLL for assembly '{assemblyName.Name}': {file.FullName}"); _dictionaryLookup[assemblyName.Name] = - new PEReader(file.Open(FileMode.Open, FileAccess.Read, FileShare.Read)); + new(file.Open(FileMode.Open, FileAccess.Read, FileShare.Read)); return _dictionaryLookup[assemblyName.Name]; } } diff --git a/UnitystationLauncher/ContentScanning/Scanners/ILScanner.cs b/UnitystationLauncher/ContentScanning/Scanners/ILScanner.cs new file mode 100644 index 00000000..52cbde71 --- /dev/null +++ b/UnitystationLauncher/ContentScanning/Scanners/ILScanner.cs @@ -0,0 +1,77 @@ +using System; +using System.Collections.Concurrent; +using System.Collections.Generic; +using System.Diagnostics; +using System.Reflection.Metadata; +using System.Reflection.PortableExecutable; +using ILVerify; +using UnitystationLauncher.Constants; +using UnitystationLauncher.Models.ConfigFile; + +namespace UnitystationLauncher.ContentScanning.Scanners; + +internal static class ILScanner +{ + internal static bool IsILValid(string name, IResolver resolver, PEReader peReader, + MetadataReader reader, Action info, Action logErrors, SandboxConfig loadedCfg) + { + info.Invoke($"{name}: Verifying IL..."); + Stopwatch sw = Stopwatch.StartNew(); + ConcurrentBag bag = new(); + + IlScanning(reader, resolver, peReader, bag); + + bool verifyErrors = false; + foreach (VerificationResult res in bag) + { + bool error = AssemblyTypeCheckerHelpers.CheckVerificationResult(loadedCfg, res, name, reader, logErrors); + if (error) + { + verifyErrors = true; + } + } + + info.Invoke($"{name}: Verified IL in {sw.Elapsed.TotalMilliseconds}ms"); + + if (verifyErrors) + { + return false; + } + + return true; + } + + // TODO: We should probably just remove this if we aren't going to use it + //private static void ParallelIlScanning(MetadataReader reader, IResolver resolver, PEReader peReader, ConcurrentBag bag) + //{ + // OrderablePartitioner partitioner = Partitioner.Create(reader.TypeDefinitions); + // Parallel.ForEach(partitioner.GetPartitions(Environment.ProcessorCount), handle => + // { + // Verifier ver = new(resolver); + // ver.SetSystemModuleName(new(AssemblyNames.SystemAssemblyName)); + // while (handle.MoveNext()) + // { + // foreach (VerificationResult? result in ver.Verify(peReader, handle.Current, verifyMethods: true)) + // { + // bag.Add(result); + // } + // } + // }); + //} + + // Using the Non-parallel implementation of this + private static void IlScanning(MetadataReader reader, IResolver resolver, PEReader peReader, ConcurrentBag bag) + { + Verifier ver = new(resolver); + //mscorlib + ver.SetSystemModuleName(new(AssemblyNames.SystemAssemblyName)); + foreach (TypeDefinitionHandle definition in reader.TypeDefinitions) + { + IEnumerable errors = ver.Verify(peReader, definition, verifyMethods: true); + foreach (VerificationResult error in errors) + { + bag.Add(error); + } + } + } +} \ No newline at end of file diff --git a/UnitystationLauncher/ContentScanning/Scanners/InheritanceScanner.cs b/UnitystationLauncher/ContentScanning/Scanners/InheritanceScanner.cs new file mode 100644 index 00000000..20f5d701 --- /dev/null +++ b/UnitystationLauncher/ContentScanning/Scanners/InheritanceScanner.cs @@ -0,0 +1,53 @@ +using System; +using System.Collections.Concurrent; +using System.Collections.Generic; +using UnitystationLauncher.Models.ConfigFile; +using UnitystationLauncher.Models.ContentScanning; +using UnitystationLauncher.Models.ContentScanning.ScanningTypes; +using UnitystationLauncher.Models.Enums; + +namespace UnitystationLauncher.ContentScanning.Scanners; + +internal static class InheritanceScanner +{ + internal static void CheckInheritance( + SandboxConfig sandboxConfig, + List<(MType type, MType parent, ArraySegment interfaceImpls)> inherited, + ConcurrentBag errors) + { + // This inheritance whitelisting primarily serves to avoid content doing funny stuff + // by e.g. inheriting Type. + foreach ((MType _, MType baseType, ArraySegment interfaces) in inherited) + { + if (CanInherit(baseType) == false) + { + errors.Add(new($"Inheriting of type not allowed: {baseType}")); + } + + foreach (MType @interface in interfaces) + { + if (CanInherit(@interface) == false) + { + errors.Add(new($"Implementing of interface not allowed: {@interface}")); + } + } + + bool CanInherit(MType inheritType) + { + MTypeReferenced realBaseType = inheritType switch + { + MTypeGeneric generic => (MTypeReferenced)generic.GenericType, + MTypeReferenced referenced => referenced, + _ => throw new InvalidOperationException() // Can't happen. + }; + + if (realBaseType.IsTypeAccessAllowed(sandboxConfig, out TypeConfig? cfg) == false) + { + return false; + } + + return cfg.Inherit != InheritMode.Block && (cfg.Inherit == InheritMode.Allow || cfg.All); + } + } + } +} \ No newline at end of file diff --git a/UnitystationLauncher/ContentScanning/Scanners/MemberReferenceScanner.cs b/UnitystationLauncher/ContentScanning/Scanners/MemberReferenceScanner.cs new file mode 100644 index 00000000..fda3e62b --- /dev/null +++ b/UnitystationLauncher/ContentScanning/Scanners/MemberReferenceScanner.cs @@ -0,0 +1,237 @@ +using System; +using System.Collections.Concurrent; +using System.Collections.Generic; +using System.Linq; +using System.Threading.Tasks; +using UnitystationLauncher.Models.ConfigFile; +using UnitystationLauncher.Models.ContentScanning; +using UnitystationLauncher.Models.ContentScanning.ScanningTypes; + +namespace UnitystationLauncher.ContentScanning.Scanners; + +internal static class MemberReferenceScanner +{ + // Using the Parallel implementation of this + internal static void CheckMemberReferences(SandboxConfig sandboxConfig, IEnumerable members, ConcurrentBag errors) + { + Parallel.ForEach(members, memberRef => + { + MTypeReferenced? baseTypeReferenced = GetBaseMTypeReferenced(memberRef); + + if (baseTypeReferenced == null) + { + return; + } + + if (baseTypeReferenced.IsTypeAccessAllowed(sandboxConfig, out TypeConfig? typeCfg) == false) + { + // Technically this error isn't necessary since we have an earlier pass + // checking all referenced types. That should have caught this + // We still need the typeCfg so that's why we're checking. Might as well. + errors.Add(new($"Access to type not allowed: {baseTypeReferenced}")); + return; + } + + if (typeCfg.All) + { + // Fully whitelisted for the type, we good. + return; + } + + CheckMemberRefType(memberRef, typeCfg, errors); + }); + + } + + private static void CheckMemberRefType(MMemberRef memberRef, TypeConfig typeCfg, ConcurrentBag errors) + { + switch (memberRef) + { + case MMemberRefField mMemberRefField + when typeCfg.FieldsParsed.Any(field => field.Name == mMemberRefField.Name + && mMemberRefField.FieldType.WhitelistEquals(field.FieldType)): + return; // Found + case MMemberRefField mMemberRefField: + errors.Add(new($"Access to field not allowed: {mMemberRefField}")); + return; + case MMemberRefMethod mMemberRefMethod: + bool safe = IsMMemberRefMethodSafe(mMemberRefMethod, typeCfg); + if (!safe) + { + errors.Add(new($"Access to method not allowed: {mMemberRefMethod}")); + } + + return; + default: + throw new ArgumentException($"Invalid memberRef type: {memberRef.GetType()}", nameof(memberRef)); + } + } + + private static bool IsMMemberRefMethodSafe(MMemberRefMethod mMemberRefMethod, TypeConfig typeCfg) + { + foreach (WhitelistMethodDefine parsed in typeCfg.MethodsParsed) + { + bool paramMismatch = false; + if (parsed.Name != mMemberRefMethod.Name || + !mMemberRefMethod.ReturnType.WhitelistEquals(parsed.ReturnType) || + mMemberRefMethod.ParameterTypes.Length != parsed.ParameterTypes.Count || + mMemberRefMethod.GenericParameterCount != parsed.GenericParameterCount) + { + continue; + } + + for (int i = 0; i < mMemberRefMethod.ParameterTypes.Length; i++) + { + MType a = mMemberRefMethod.ParameterTypes[i]; + MType b = parsed.ParameterTypes[i]; + + if (a.WhitelistEquals(b)) + { + continue; + } + + paramMismatch = true; + break; + } + if (!paramMismatch) + { + return true; // Found + } + } + + return false; + } + + + private static MTypeReferenced? GetBaseMTypeReferenced(MMemberRef memberRef) + { + MType baseType = memberRef.ParentType; + while (baseType is not MTypeReferenced) + { + switch (baseType) + { + case MTypeGeneric generic: + baseType = generic.GenericType; + continue; + // MTypeWackyArray: Members on arrays (not to be confused with vectors) are all fine. + // See II.14.2 in ECMA-335. + // MTypeDefined: Valid for this to show up, safe to ignore. + case MTypeWackyArray or MTypeDefined: + return null; + default: + throw new ArgumentException($"Invalid baseType in memberRef: {baseType.GetType()}", nameof(memberRef)); + } + } + + return (MTypeReferenced)baseType; + } + + // TODO: We should probably just remove this if we aren't going to use it + //private static void NonParallelCheckMemberReferences(SandboxConfig sandboxConfig, List members, ConcurrentBag errors) + //{ + // foreach (MMemberRef memberRef in members) + // { + // MType baseType = memberRef.ParentType; + // while (baseType is not MTypeReferenced) + // { + // switch (baseType) + // { + // case MTypeGeneric generic: + // { + // baseType = generic.GenericType; + // + // break; + // } + // case MTypeWackyArray: + // { + // // Members on arrays (not to be confused with vectors) are all fine. + // // See II.14.2 in ECMA-335. + // continue; + // } + // case MTypeDefined: + // { + // // Valid for this to show up, safe to ignore. + // continue; + // } + // default: + // { + // throw new ArgumentOutOfRangeException(); + // } + // } + // } + // + // MTypeReferenced baseTypeReferenced = (MTypeReferenced)baseType; + // + // if (baseTypeReferenced.IsTypeAccessAllowed(sandboxConfig, out TypeConfig? typeCfg) == false) + // { + // // Technically this error isn't necessary since we have an earlier pass + // // checking all referenced types. That should have caught this + // // We still need the typeCfg so that's why we're checking. Might as well. + // errors.Add(new($"Access to type not allowed: {baseTypeReferenced}")); + // continue; + // } + // + // if (typeCfg.All) + // { + // // Fully whitelisted for the type, we good. + // continue; + // } + // + // switch (memberRef) + // { + // case MMemberRefField mMemberRefField: + // { + // foreach (WhitelistFieldDefine field in typeCfg.FieldsParsed) + // { + // if (field.Name == mMemberRefField.Name && + // mMemberRefField.FieldType.WhitelistEquals(field.FieldType)) + // { + // continue; // Found + // } + // } + // + // errors.Add(new($"Access to field not allowed: {mMemberRefField}")); + // break; + // } + // case MMemberRefMethod mMemberRefMethod: + // bool paramMismatch = false; + // foreach (WhitelistMethodDefine parsed in typeCfg.MethodsParsed) + // { + // if (parsed.Name != mMemberRefMethod.Name || + // !mMemberRefMethod.ReturnType.WhitelistEquals(parsed.ReturnType) || + // mMemberRefMethod.ParameterTypes.Length != parsed.ParameterTypes.Count || + // mMemberRefMethod.GenericParameterCount != parsed.GenericParameterCount) + // { + // continue; + // } + // + // for (int i = 0; i < mMemberRefMethod.ParameterTypes.Length; i++) + // { + // MType a = mMemberRefMethod.ParameterTypes[i]; + // MType b = parsed.ParameterTypes[i]; + // + // if (a.WhitelistEquals(b)) + // { + // continue; + // } + // + // paramMismatch = true; + // break; + // } + // + // break; + // } + // + // if (paramMismatch) + // { + // continue; + // } + // + // errors.Add(new($"Access to method not allowed: {mMemberRefMethod}")); + // break; + // default: + // throw new ArgumentOutOfRangeException(nameof(memberRef)); + // } + // } + //} +} \ No newline at end of file diff --git a/UnitystationLauncher/ContentScanning/Scanners/TypeAbuseScanner.cs b/UnitystationLauncher/ContentScanning/Scanners/TypeAbuseScanner.cs new file mode 100644 index 00000000..d43c214a --- /dev/null +++ b/UnitystationLauncher/ContentScanning/Scanners/TypeAbuseScanner.cs @@ -0,0 +1,31 @@ +using System.Collections.Concurrent; +using System.Reflection; +using System.Reflection.Metadata; +using UnitystationLauncher.Infrastructure; +using UnitystationLauncher.Models.ContentScanning.ScanningTypes; + +namespace UnitystationLauncher.ContentScanning.Scanners; + +internal static class TypeAbuseScanner +{ + internal static void CheckNoTypeAbuse(MetadataReader reader, ConcurrentBag errors) + { + foreach (TypeDefinitionHandle typeDefHandle in reader.TypeDefinitions) + { + TypeDefinition typeDef = reader.GetTypeDefinition(typeDefHandle); + if ((typeDef.Attributes & TypeAttributes.ExplicitLayout) != 0) + { + // The C# compiler emits explicit layout types for some array init logic. These have no fields. + // Only ban explicit layout if it has fields. + + MTypeDefined type = reader.GetTypeFromDefinition(typeDefHandle); + + if (typeDef.GetFields().Count > 0) + { + string err = $"Explicit layout type {type} may not have fields."; + errors.Add(new(err)); + } + } + } + } +} \ No newline at end of file diff --git a/UnitystationLauncher/ContentScanning/Scanners/UnmanagedMethodScanner.cs b/UnitystationLauncher/ContentScanning/Scanners/UnmanagedMethodScanner.cs new file mode 100644 index 00000000..f11173ad --- /dev/null +++ b/UnitystationLauncher/ContentScanning/Scanners/UnmanagedMethodScanner.cs @@ -0,0 +1,33 @@ +using System.Collections.Concurrent; +using System.Reflection; +using System.Reflection.Metadata; +using UnitystationLauncher.Infrastructure; + +namespace UnitystationLauncher.ContentScanning.Scanners; + +internal static class UnmanagedMethodScanner +{ + internal static void CheckNoUnmanagedMethodDefs(MetadataReader reader, ConcurrentBag errors) + { + foreach (MethodDefinitionHandle methodDefHandle in reader.MethodDefinitions) + { + MethodDefinition methodDef = reader.GetMethodDefinition(methodDefHandle); + MethodImplAttributes implAttr = methodDef.ImplAttributes; + MethodAttributes attr = methodDef.Attributes; + + if ((implAttr & MethodImplAttributes.Unmanaged) != 0 || + (implAttr & MethodImplAttributes.CodeTypeMask) is not (MethodImplAttributes.IL + or MethodImplAttributes.Runtime)) + { + string err = $"Method has illegal MethodImplAttributes: {reader.FormatMethodName(methodDef)}"; + errors.Add(new(err)); + } + + if ((attr & (MethodAttributes.PinvokeImpl | MethodAttributes.UnmanagedExport)) != 0) + { + string err = $"Method has illegal MethodAttributes: {reader.FormatMethodName(methodDef)}"; + errors.Add(new(err)); + } + } + } +} \ No newline at end of file diff --git a/UnitystationLauncher/ContentScanning/TypeProvider.cs b/UnitystationLauncher/ContentScanning/TypeProvider.cs index 5483dc6b..b0618f6f 100644 --- a/UnitystationLauncher/ContentScanning/TypeProvider.cs +++ b/UnitystationLauncher/ContentScanning/TypeProvider.cs @@ -1,7 +1,7 @@ - using System; using System.Collections.Immutable; using System.Reflection.Metadata; +using UnitystationLauncher.Infrastructure; using UnitystationLauncher.Models.ContentScanning; using UnitystationLauncher.Models.ContentScanning.ScanningTypes; @@ -11,7 +11,7 @@ internal sealed class TypeProvider : ISignatureTypeProvider { public MType GetSZArrayType(MType elementType) { - return new MTypeSZArray(elementType); + return new MTypeSzArray(elementType); } public MType GetArrayType(MType elementType, ArrayShape shape) @@ -41,12 +41,12 @@ public MType GetPrimitiveType(PrimitiveTypeCode typeCode) public MType GetTypeFromDefinition(MetadataReader reader, TypeDefinitionHandle handle, byte rawTypeKind) { - return AssemblyTypeCheckerHelpers.GetTypeFromDefinition(reader, handle); + return reader.GetTypeFromDefinition(handle); } public MType GetTypeFromReference(MetadataReader inReader, TypeReferenceHandle inHandle, byte inRawTypeKind) { - return AssemblyTypeCheckerHelpers.ParseTypeReference(inReader, inHandle); + return inReader.ParseTypeReference(inHandle); } public MType GetFunctionPointerType(MethodSignature signature) diff --git a/UnitystationLauncher/Exceptions/CodeScanningException.cs b/UnitystationLauncher/Exceptions/CodeScanningException.cs new file mode 100644 index 00000000..9377699f --- /dev/null +++ b/UnitystationLauncher/Exceptions/CodeScanningException.cs @@ -0,0 +1,18 @@ +using System; + +namespace UnitystationLauncher.Exceptions; + +public sealed class CodeScanningException : Exception +{ + public CodeScanningException() + { + } + + public CodeScanningException(string message) : base(message) + { + } + + public CodeScanningException(string message, Exception inner) : base(message, inner) + { + } +} \ No newline at end of file diff --git a/UnitystationLauncher/Exceptions/ContentLengthNullException.cs b/UnitystationLauncher/Exceptions/ContentLengthNullException.cs index e3c02e17..da4c20f5 100644 --- a/UnitystationLauncher/Exceptions/ContentLengthNullException.cs +++ b/UnitystationLauncher/Exceptions/ContentLengthNullException.cs @@ -1,28 +1,27 @@ using System; -namespace UnitystationLauncher.Exceptions +namespace UnitystationLauncher.Exceptions; + +public class ContentLengthNullException : Exception { - public class ContentLengthNullException : Exception + public ContentLengthNullException(string url) { - public ContentLengthNullException(string url) - { - Url = url; - } + Url = url; + } - public ContentLengthNullException(string url, string message) - : base(message) - { - Url = url; - } + public ContentLengthNullException(string url, string message) + : base(message) + { + Url = url; + } - public ContentLengthNullException(string url, string message, Exception inner) - : base(message, inner) - { - Url = url; - } + public ContentLengthNullException(string url, string message, Exception inner) + : base(message, inner) + { + Url = url; + } - public string Url { get; } + public string Url { get; } - public override String Message => $"{base.Message} Url='{Url}"; - } -} \ No newline at end of file + public override String Message => $"{base.Message} Url='{Url}"; +} diff --git a/UnitystationLauncher/Exceptions/UnsupportedPlatformException.cs b/UnitystationLauncher/Exceptions/UnsupportedPlatformException.cs new file mode 100644 index 00000000..24c07469 --- /dev/null +++ b/UnitystationLauncher/Exceptions/UnsupportedPlatformException.cs @@ -0,0 +1,18 @@ +using System; + +namespace UnitystationLauncher.Exceptions; + +public sealed class UnsupportedPlatformException : Exception +{ + public UnsupportedPlatformException() + { + } + + public UnsupportedPlatformException(string message) : base(message) + { + } + + public UnsupportedPlatformException(string message, Exception inner) : base(message, inner) + { + } +} \ No newline at end of file diff --git a/UnitystationLauncher/GameCommunicationPipe/PipeHubBuildCommunication.cs b/UnitystationLauncher/GameCommunicationPipe/PipeHubBuildCommunication.cs index 71c040fd..07762048 100644 --- a/UnitystationLauncher/GameCommunicationPipe/PipeHubBuildCommunication.cs +++ b/UnitystationLauncher/GameCommunicationPipe/PipeHubBuildCommunication.cs @@ -19,15 +19,15 @@ public class PipeHubBuildCommunication : IDisposable public PipeHubBuildCommunication() { - _serverPipe = new NamedPipeServerStream("Unitystation_Hub_Build_Communication", PipeDirection.InOut, 1, + _serverPipe = new("Unitystation_Hub_Build_Communication", PipeDirection.InOut, 1, PipeTransmissionMode.Byte, PipeOptions.Asynchronous); } public async Task StartServerPipe() { await _serverPipe.WaitForConnectionAsync(); - _reader = new StreamReader(_serverPipe); - _writer = new StreamWriter(_serverPipe); + _reader = new(_serverPipe); + _writer = new(_serverPipe); while (true) { @@ -42,14 +42,14 @@ public async Task StartServerPipe() { Log.Error(e.ToString()); _serverPipe.Close(); - _serverPipe = new NamedPipeServerStream("Unitystation_Hub_Build_Communication", PipeDirection.InOut, + _serverPipe = new("Unitystation_Hub_Build_Communication", PipeDirection.InOut, 1, PipeTransmissionMode.Byte, PipeOptions.Asynchronous); await _serverPipe.WaitForConnectionAsync(); } - _reader = new StreamReader(_serverPipe); - _writer = new StreamWriter(_serverPipe); + _reader = new(_serverPipe); + _writer = new(_serverPipe); continue; } diff --git a/UnitystationLauncher/Infrastructure/MetadataReaderExtensions.cs b/UnitystationLauncher/Infrastructure/MetadataReaderExtensions.cs new file mode 100644 index 00000000..3dc54761 --- /dev/null +++ b/UnitystationLauncher/Infrastructure/MetadataReaderExtensions.cs @@ -0,0 +1,290 @@ +using System; +using System.Collections.Concurrent; +using System.Collections.Generic; +using System.Linq; +using System.Reflection.Metadata; +using UnitystationLauncher.ContentScanning; +using UnitystationLauncher.Exceptions; +using UnitystationLauncher.Models.ContentScanning; +using UnitystationLauncher.Models.ContentScanning.ScanningTypes; + +namespace UnitystationLauncher.Infrastructure; + +internal static class MetadataReaderExtensions +{ + internal static List<(MType type, MType parent, ArraySegment interfaceImpls)> GetExternalInheritedTypes( + this MetadataReader reader, + ConcurrentBag errors) + { + List<(MType, MType, ArraySegment)> list = new(); + foreach (TypeDefinitionHandle typeDefHandle in reader.TypeDefinitions) + { + TypeDefinition typeDef = reader.GetTypeDefinition(typeDefHandle); + ArraySegment interfaceImpls; + MTypeDefined type = reader.GetTypeFromDefinition(typeDefHandle); + + if (!type.ParseInheritType(typeDef.BaseType, out MType? parent, reader, errors)) + { + continue; + } + + InterfaceImplementationHandleCollection interfaceImplsCollection = typeDef.GetInterfaceImplementations(); + if (interfaceImplsCollection.Count == 0) + { + interfaceImpls = Array.Empty(); + list.Add((type, parent, interfaceImpls)); + break; + } + + interfaceImpls = new MType[interfaceImplsCollection.Count]; + int i = 0; + foreach (InterfaceImplementation interfaceImpl in interfaceImplsCollection.Select(reader.GetInterfaceImplementation)) + { + if (type.ParseInheritType(interfaceImpl.Interface, out MType? implemented, reader, errors)) + { + interfaceImpls[i++] = implemented; + } + } + + interfaceImpls = interfaceImpls[..i]; + + list.Add((type, parent, interfaceImpls)); + } + + return list; + } + + internal static List GetReferencedTypes(this MetadataReader reader, ConcurrentBag errors) + { + return reader.TypeReferences.Select(typeRefHandle => + { + try + { + return reader.ParseTypeReference(typeRefHandle); + } + catch (UnsupportedMetadataException e) + { + errors.Add(new(e)); + return null; + } + }) + .Where(p => p != null) + .ToList()!; + } + + /// + /// Thrown if the metadata does something funny we don't "support" like type forwarding. + /// + internal static MTypeReferenced ParseTypeReference(this MetadataReader reader, TypeReferenceHandle handle) + { + TypeReference typeRef = reader.GetTypeReference(handle); + string name = reader.GetString(typeRef.Name); + string? nameSpace = typeRef.Namespace.NilNullString(reader); + MResScope resScope; + + // See II.22.38 in ECMA-335 + if (typeRef.ResolutionScope.IsNil) + { + throw new UnsupportedMetadataException( + $"Null resolution scope on type Name: {nameSpace}.{name}. This indicates exported/forwarded types"); + } + + switch (typeRef.ResolutionScope.Kind) + { + case HandleKind.AssemblyReference: + { + // Different assembly. + AssemblyReference assemblyRef = + reader.GetAssemblyReference((AssemblyReferenceHandle)typeRef.ResolutionScope); + string assemblyName = reader.GetString(assemblyRef.Name); + resScope = new MResScopeAssembly(assemblyName); + break; + } + case HandleKind.TypeReference: + { + // Nested type. + MTypeReferenced enclosingType = ParseTypeReference(reader, (TypeReferenceHandle)typeRef.ResolutionScope); + resScope = new MResScopeType(enclosingType); + break; + } + case HandleKind.ModuleReference: + { + // Same-assembly-different-module + throw new UnsupportedMetadataException( + $"Cross-module reference to type {nameSpace}.{name}. "); + } + default: + // Edge cases not handled: + // https://github.com/dotnet/runtime/blob/b2e5a89085fcd87e2fa9300b4bb00cd499c5845b/src/libraries/System.Reflection.Metadata/tests/Metadata/Decoding/DisassemblingTypeProvider.cs#L130-L132 + throw new UnsupportedMetadataException( + $"TypeRef to {typeRef.ResolutionScope.Kind} for type {nameSpace}.{name}"); + } + + return new(resScope, name, nameSpace); + } + + private static MType? GetParent(this MetadataReader reader, HandleKind kind, EntityHandle parentHandle, string memName, ConcurrentBag errors) + { + switch (kind) + { + // See II.22.25 in ECMA-335. + case HandleKind.TypeReference: + // Regular type reference. + try + { + return reader.ParseTypeReference((TypeReferenceHandle)parentHandle); + } + catch (UnsupportedMetadataException u) + { + errors.Add(new(u)); + return null; + } + case HandleKind.TypeDefinition: + try + { + return reader.GetTypeFromDefinition((TypeDefinitionHandle)parentHandle); + } + catch (UnsupportedMetadataException u) + { + errors.Add(new(u)); + return null; + } + case HandleKind.TypeSpecification: + TypeSpecification typeSpec = reader.GetTypeSpecification((TypeSpecificationHandle)parentHandle); + // Generic type reference. + TypeProvider provider = new(); + MType parent = typeSpec.DecodeSignature(provider, 0); + + if (parent.IsCoreTypeDefined()) + { + // Ensure this isn't a self-defined type. + // This can happen due to generics since MethodSpec needs to point to MemberRef. + return null; + } + + return parent; + case HandleKind.ModuleReference: + errors.Add(new( + $"Module global variables and methods are unsupported. Name: {memName}")); + return null; + case HandleKind.MethodDefinition: + errors.Add(new($"Vararg calls are unsupported. Name: {memName}")); + return null; + default: + errors.Add(new( + $"Unsupported member ref parent type: {kind}. Name: {memName}")); + return null; + } + } + + // Using the Parallel implementation of this + internal static List GetReferencedMembers(this MetadataReader reader, ConcurrentBag errors) + { + return reader.MemberReferences.AsParallel() + .Select(memRefHandle => + { + MemberReference memRef = reader.GetMemberReference(memRefHandle); + string memName = reader.GetString(memRef.Name); + MType? parent = reader.GetParent(memRef.Parent.Kind, memRef.Parent, memName, errors); + if (parent == null) + { + return null; + } + + MMemberRef memberRef; + + switch (memRef.GetKind()) + { + case MemberReferenceKind.Method: + { + MethodSignature sig = memRef.DecodeMethodSignature(new TypeProvider(), 0); + + memberRef = new MMemberRefMethod( + parent, + memName, + sig.ReturnType, + sig.GenericParameterCount, + sig.ParameterTypes); + + break; + } + case MemberReferenceKind.Field: + { + MType fieldType = memRef.DecodeFieldSignature(new TypeProvider(), 0); + memberRef = new MMemberRefField(parent, memName, fieldType); + break; + } + default: + throw new ArgumentOutOfRangeException(); + } + + return memberRef; + }) + .Where(p => p != null) + .ToList()!; + } + + // TODO: We should probably just remove this if we aren't going to use it + //private static List NonParallelReferencedMembersCheck(this MetadataReader reader, ConcurrentBag errors) + //{ + // return reader.MemberReferences.Select(memRefHandle => + // { + // MemberReference memRef = reader.GetMemberReference(memRefHandle); + // string memName = reader.GetString(memRef.Name); + // MType? parent = reader.GetParent(memRef.Parent.Kind, memRef.Parent, memName, errors); + // if (parent == null) + // { + // return null; + // } + // + // MMemberRef memberRef; + // + // switch (memRef.GetKind()) + // { + // case MemberReferenceKind.Method: + // MethodSignature sig = memRef.DecodeMethodSignature(new TypeProvider(), 0); + // + // memberRef = new MMemberRefMethod( + // parent, + // memName, + // sig.ReturnType, + // sig.GenericParameterCount, + // sig.ParameterTypes); + // + // break; + // case MemberReferenceKind.Field: + // MType fieldType = memRef.DecodeFieldSignature(new TypeProvider(), 0); + // memberRef = new MMemberRefField(parent, memName, fieldType); + // break; + // default: throw new ArgumentOutOfRangeException(); + // } + // + // return memberRef; + // }) + // .Where(p => p != null) + // .ToList()!; + //} + + internal static MTypeDefined GetTypeFromDefinition(this MetadataReader reader, TypeDefinitionHandle handle) + { + TypeDefinition typeDef = reader.GetTypeDefinition(handle); + string name = reader.GetString(typeDef.Name); + string? ns = typeDef.Namespace.NilNullString(reader); + MTypeDefined? enclosing = null; + if (typeDef.IsNested) + { + enclosing = reader.GetTypeFromDefinition(typeDef.GetDeclaringType()); + } + + return new(name, ns, enclosing); + } + + internal static string FormatMethodName(this MetadataReader reader, MethodDefinition method) + { + MethodSignature methodSig = method.DecodeSignature(new TypeProvider(), 0); + MTypeDefined type = reader.GetTypeFromDefinition(method.GetDeclaringType()); + + return + $"{type}.{reader.GetString(method.Name)}({string.Join(", ", methodSig.ParameterTypes)}) Returns {methodSig.ReturnType} "; + } +} \ No newline at end of file diff --git a/UnitystationLauncher/Infrastructure/ProgressStream.cs b/UnitystationLauncher/Infrastructure/ProgressStream.cs index f7404f78..182c8630 100644 --- a/UnitystationLauncher/Infrastructure/ProgressStream.cs +++ b/UnitystationLauncher/Infrastructure/ProgressStream.cs @@ -13,7 +13,7 @@ public ProgressStream(Stream inner) public Stream Inner { get; set; } public IObservable Progress => _progress; - private readonly Subject _progress = new Subject(); + private readonly Subject _progress = new(); private long _position; public override bool CanRead => Inner.CanRead; @@ -33,7 +33,7 @@ public override void Flush() public override int Read(byte[] buffer, int offset, int count) { - var r = Inner.Read(buffer, offset, count); + int r = Inner.Read(buffer, offset, count); _position += r; _progress.OnNext(_position); return r; diff --git a/UnitystationLauncher/Infrastructure/StringExtensions.cs b/UnitystationLauncher/Infrastructure/StringExtensions.cs new file mode 100644 index 00000000..a4dcbbee --- /dev/null +++ b/UnitystationLauncher/Infrastructure/StringExtensions.cs @@ -0,0 +1,9 @@ +namespace UnitystationLauncher.Infrastructure; + +public static class StringExtensions +{ + public static string SanitiseStringPath(this string input) + { + return input.Replace(@"\", "").Replace("/", "").Replace(".", "_"); + } +} \ No newline at end of file diff --git a/UnitystationLauncher/Infrastructure/StringHandleExtensions.cs b/UnitystationLauncher/Infrastructure/StringHandleExtensions.cs new file mode 100644 index 00000000..6c4fd0ff --- /dev/null +++ b/UnitystationLauncher/Infrastructure/StringHandleExtensions.cs @@ -0,0 +1,11 @@ +using System.Reflection.Metadata; + +namespace UnitystationLauncher.Infrastructure; + +internal static class StringHandleExtensions +{ + internal static string? NilNullString(this StringHandle handle, MetadataReader reader) + { + return handle.IsNil ? null : reader.GetString(handle); + } +} \ No newline at end of file diff --git a/UnitystationLauncher/Infrastructure/TypeExtensions.cs b/UnitystationLauncher/Infrastructure/TypeExtensions.cs index b6b29725..2b31ab3a 100644 --- a/UnitystationLauncher/Infrastructure/TypeExtensions.cs +++ b/UnitystationLauncher/Infrastructure/TypeExtensions.cs @@ -19,7 +19,7 @@ public static IEnumerable DumpMetaMembers(this Type type) // Load assembly with System.Reflection.Metadata. using FileStream fs = File.OpenRead(assemblyLoc); - using PEReader peReader = new PEReader(fs); + using PEReader peReader = new(fs); MetadataReader metaReader = peReader.GetMetadataReader(); @@ -31,7 +31,7 @@ public static IEnumerable DumpMetaMembers(this Type type) { TypeDefinition tempTypeDef = metaReader.GetTypeDefinition(typeDefHandle); string name = metaReader.GetString(tempTypeDef.Name); - string? @namespace = AssemblyTypeCheckerHelpers.NilNullString(metaReader, tempTypeDef.Namespace); + string? @namespace = tempTypeDef.Namespace.NilNullString(metaReader); if (name == type.Name && @namespace == type.Namespace) { typeDef = tempTypeDef; diff --git a/UnitystationLauncher/Models/Api/Changelog/BlogPost.cs b/UnitystationLauncher/Models/Api/Changelog/BlogPost.cs index 17f5bce1..1c55bcd5 100644 --- a/UnitystationLauncher/Models/Api/Changelog/BlogPost.cs +++ b/UnitystationLauncher/Models/Api/Changelog/BlogPost.cs @@ -1,7 +1,6 @@ using System; using System.Collections.Generic; using System.Text.Json.Serialization; -using Avalonia.Media.Imaging; namespace UnitystationLauncher.Models.Api.Changelog; diff --git a/UnitystationLauncher/Models/Api/Server.cs b/UnitystationLauncher/Models/Api/Server.cs index 6a3be60b..6317838f 100644 --- a/UnitystationLauncher/Models/Api/Server.cs +++ b/UnitystationLauncher/Models/Api/Server.cs @@ -1,7 +1,5 @@ using System; -using System.IO; using System.Net; -using UnitystationLauncher.Models.ConfigFile; using UnitystationLauncher.Models.Enums; using UnitystationLauncher.Services.Interface; @@ -86,7 +84,7 @@ public override bool Equals(object? obj) public override int GetHashCode() { - var hash = new HashCode(); + HashCode hash = new(); hash.Add(ServerIp); hash.Add(ServerPort); return hash.ToHashCode(); diff --git a/UnitystationLauncher/Models/ConfigFile/SandboxConfig.cs b/UnitystationLauncher/Models/ConfigFile/SandboxConfig.cs index 47182577..bbdb4384 100644 --- a/UnitystationLauncher/Models/ConfigFile/SandboxConfig.cs +++ b/UnitystationLauncher/Models/ConfigFile/SandboxConfig.cs @@ -9,10 +9,9 @@ namespace UnitystationLauncher.Models.ConfigFile; public sealed class SandboxConfig { public string? SystemAssemblyName { get; set; } - public List AllowedVerifierErrors { get; set; } = new List(); - public List WhitelistedNamespaces { get; set; } = new List(); - public List MultiAssemblyOtherReferences { get; set; } = new List(); + public List AllowedVerifierErrors { get; set; } = new(); + public List WhitelistedNamespaces { get; set; } = new(); + public List MultiAssemblyOtherReferences { get; set; } = new(); - public Dictionary> Types { get; set; } = - new Dictionary>(); + public Dictionary> Types { get; set; } = new(); } \ No newline at end of file diff --git a/UnitystationLauncher/Models/ContentScanning/MType.cs b/UnitystationLauncher/Models/ContentScanning/MType.cs index b56b42fe..004eee60 100644 --- a/UnitystationLauncher/Models/ContentScanning/MType.cs +++ b/UnitystationLauncher/Models/ContentScanning/MType.cs @@ -1,4 +1,11 @@ -namespace UnitystationLauncher.Models.ContentScanning; +using System.Collections.Concurrent; +using System.Diagnostics.CodeAnalysis; +using System.Reflection.Metadata; +using UnitystationLauncher.ContentScanning; +using UnitystationLauncher.Exceptions; +using UnitystationLauncher.Infrastructure; + +namespace UnitystationLauncher.Models.ContentScanning; public record MType @@ -20,5 +27,53 @@ public virtual bool IsCoreTypeDefined() { return ToString(); } + + internal bool ParseInheritType(EntityHandle handle, [NotNullWhen(true)] out MType? type, MetadataReader reader, ConcurrentBag errors) + { + type = default; + + switch (handle.Kind) + { + case HandleKind.TypeDefinition: + // Definition to type in same assembly, allowed without hassle. + return false; + + case HandleKind.TypeReference: + // Regular type reference. + try + { + type = reader.ParseTypeReference((TypeReferenceHandle)handle); + return true; + } + catch (UnsupportedMetadataException u) + { + errors.Add(new(u)); + return false; + } + + case HandleKind.TypeSpecification: + TypeSpecification typeSpec = reader.GetTypeSpecification((TypeSpecificationHandle)handle); + // Generic type reference. + TypeProvider provider = new(); + type = typeSpec.DecodeSignature(provider, 0); + + if (type.IsCoreTypeDefined()) + { + // Ensure this isn't a self-defined type. + // This can happen due to generics. + return false; + } + + break; + + default: + errors.Add(new( + $"Unsupported BaseType of kind {handle.Kind} on type {this}")); + return false; + } + + type = default!; + return false; + } } diff --git a/UnitystationLauncher/Models/ContentScanning/ScanningTypes/MTypeGeneric.cs b/UnitystationLauncher/Models/ContentScanning/ScanningTypes/MTypeGeneric.cs index 6bc8a848..e64d7134 100644 --- a/UnitystationLauncher/Models/ContentScanning/ScanningTypes/MTypeGeneric.cs +++ b/UnitystationLauncher/Models/ContentScanning/ScanningTypes/MTypeGeneric.cs @@ -51,7 +51,7 @@ public bool Equals(MTypeGeneric? otherGeneric) public override int GetHashCode() { - HashCode hc = new HashCode(); + HashCode hc = new(); hc.Add(GenericType); foreach (MType typeArg in TypeArguments) { diff --git a/UnitystationLauncher/Models/ContentScanning/ScanningTypes/MTypeReferenced.cs b/UnitystationLauncher/Models/ContentScanning/ScanningTypes/MTypeReferenced.cs index d244824f..4470e29f 100644 --- a/UnitystationLauncher/Models/ContentScanning/ScanningTypes/MTypeReferenced.cs +++ b/UnitystationLauncher/Models/ContentScanning/ScanningTypes/MTypeReferenced.cs @@ -1,3 +1,8 @@ +using System.Collections.Generic; +using System.Diagnostics.CodeAnalysis; +using System.Linq; +using UnitystationLauncher.Models.ConfigFile; + namespace UnitystationLauncher.Models.ContentScanning.ScanningTypes; internal sealed record MTypeReferenced(MResScope ResolutionScope, string Name, string? Namespace) : MType @@ -34,4 +39,87 @@ public override bool WhitelistEquals(MType other) _ => false }; } + + public bool IsTypeAccessAllowed(SandboxConfig sandboxConfig, [NotNullWhen(true)] out TypeConfig? cfg) + { + if (Namespace == null) + { + bool? isAllowed = IsTypeAccessAllowedForTypeWithNoNamespace(sandboxConfig, out TypeConfig? noNamespaceTypeConfig); + if (isAllowed.HasValue) + { + cfg = isAllowed.Value ? noNamespaceTypeConfig : null; + return isAllowed.Value; + } + } + + // Check if in whitelisted namespaces. + if (sandboxConfig.WhitelistedNamespaces.Any(whNamespace => Namespace?.StartsWith(whNamespace) ?? false)) + { + cfg = TypeConfig.DefaultAll; + return true; + } + + if (ResolutionScope is MResScopeAssembly resScopeAssembly && + sandboxConfig.MultiAssemblyOtherReferences.Contains(resScopeAssembly.Name)) + { + cfg = TypeConfig.DefaultAll; + return true; + } + + + if (Namespace == null || sandboxConfig.Types.TryGetValue(Namespace, out Dictionary? nsDict) == false) + { + cfg = null; + return false; + } + + return nsDict.TryGetValue(Name, out cfg); + } + + private bool? IsTypeAccessAllowedForTypeWithNoNamespace(SandboxConfig sandboxConfig, [NotNullWhen(true)] out TypeConfig? cfg) + { + if (ResolutionScope is MResScopeType parentType) + { + if (parentType.Type is MTypeReferenced parentReferencedType) + { + if (parentReferencedType.IsTypeAccessAllowed(sandboxConfig, out TypeConfig? parentCfg) == false) + { + cfg = null; + return false; + } + + if (parentCfg.All) + { + // Enclosing type is namespace-whitelisted so we don't have to check anything else. + cfg = TypeConfig.DefaultAll; + return true; + } + + // Found enclosing type, checking if we are allowed to access this nested type. + // Also pass it up in case of multiple nested types. + if (parentCfg.NestedTypes != null && parentCfg.NestedTypes.TryGetValue(Name, out cfg)) + { + return true; + } + + cfg = null; + return false; + } + + if (ResolutionScope is MResScopeAssembly mResScopeAssembly && + sandboxConfig.MultiAssemblyOtherReferences.Contains(mResScopeAssembly.Name)) + { + cfg = TypeConfig.DefaultAll; + return true; + } + + // Types without namespaces or nesting parent are not allowed at all. + cfg = null; + return false; + } + + // Null means we don't have an explicit yes or no to is it allowed yet, continue checking after this method + cfg = TypeConfig.DefaultAll; + return null; + } } \ No newline at end of file diff --git a/UnitystationLauncher/Models/ContentScanning/ScanningTypes/MTypeSZArray.cs b/UnitystationLauncher/Models/ContentScanning/ScanningTypes/MTypeSZArray.cs index 1b2a4d3e..9930c06a 100644 --- a/UnitystationLauncher/Models/ContentScanning/ScanningTypes/MTypeSZArray.cs +++ b/UnitystationLauncher/Models/ContentScanning/ScanningTypes/MTypeSZArray.cs @@ -1,7 +1,7 @@ namespace UnitystationLauncher.Models.ContentScanning.ScanningTypes; // Normal single dimensional array with zero lower bound. -internal sealed record MTypeSZArray(MType ElementType) : MType +internal sealed record MTypeSzArray(MType ElementType) : MType { public override string ToString() { @@ -15,6 +15,6 @@ public override string WhitelistToString() public override bool WhitelistEquals(MType other) { - return other is MTypeSZArray arr && ElementType.WhitelistEquals(arr.ElementType); + return other is MTypeSzArray arr && ElementType.WhitelistEquals(arr.ElementType); } } \ No newline at end of file diff --git a/UnitystationLauncher/Models/ContentScanning/ScanningTypes/MTypeWackyArray.cs b/UnitystationLauncher/Models/ContentScanning/ScanningTypes/MTypeWackyArray.cs index 5aa08cbd..bf45d0d5 100644 --- a/UnitystationLauncher/Models/ContentScanning/ScanningTypes/MTypeWackyArray.cs +++ b/UnitystationLauncher/Models/ContentScanning/ScanningTypes/MTypeWackyArray.cs @@ -1,4 +1,3 @@ -using System; using System.Linq; using System.Reflection.Metadata; diff --git a/UnitystationLauncher/Models/ContentScanning/TypeConfig.cs b/UnitystationLauncher/Models/ContentScanning/TypeConfig.cs index 3c8e2b5f..c904f63b 100644 --- a/UnitystationLauncher/Models/ContentScanning/TypeConfig.cs +++ b/UnitystationLauncher/Models/ContentScanning/TypeConfig.cs @@ -8,7 +8,7 @@ public sealed class TypeConfig { // Used for type configs where the type config doesn't exist due to a bigger-scoped All whitelisting. // e.g. nested types or namespace whitelist. - public static readonly TypeConfig DefaultAll = new TypeConfig { All = true }; + public static readonly TypeConfig DefaultAll = new() { All = true }; public bool All { get; set; } public InheritMode Inherit { get; set; } = InheritMode.Default; diff --git a/UnitystationLauncher/Models/Download.cs b/UnitystationLauncher/Models/Download.cs index 1d63b2bc..12cba188 100644 --- a/UnitystationLauncher/Models/Download.cs +++ b/UnitystationLauncher/Models/Download.cs @@ -1,5 +1,6 @@ using System; using ReactiveUI; +using UnitystationLauncher.Models.Enums; namespace UnitystationLauncher.Models { @@ -43,13 +44,24 @@ public long Downloaded public int Progress => (int)(Downloaded * 100 / Math.Max(1, Size)); - public Download(string url, string installationPath, string forkName, int buildVersion, string inGoodFileVersion) + private DownloadState _downloadState; + public DownloadState DownloadState + { + get => _downloadState; + set => this.RaiseAndSetIfChanged(ref _downloadState, value); + } + + public Download(string url, string installationPath, string forkName, int buildVersion, string inGoodFileVersion, DownloadState? downloadState = null) { DownloadUrl = url; InstallPath = installationPath; ForkName = forkName; BuildVersion = buildVersion; GoodFileVersion = inGoodFileVersion; + if (downloadState != null) + { + DownloadState = downloadState.Value; + } } } } \ No newline at end of file diff --git a/UnitystationLauncher/Models/Enums/DownloadState.cs b/UnitystationLauncher/Models/Enums/DownloadState.cs new file mode 100644 index 00000000..a7acd417 --- /dev/null +++ b/UnitystationLauncher/Models/Enums/DownloadState.cs @@ -0,0 +1,14 @@ +using System.ComponentModel; + +namespace UnitystationLauncher.Models.Enums; + +[DefaultValue(NotDownloaded)] +public enum DownloadState +{ + + NotDownloaded, + InProgress, + Scanning, + Installed, + Failed +} \ No newline at end of file diff --git a/UnitystationLauncher/Services/AssemblyTypeCheckerService.cs b/UnitystationLauncher/Services/AssemblyTypeCheckerService.cs index 49fa87e2..52b1f08e 100644 --- a/UnitystationLauncher/Services/AssemblyTypeCheckerService.cs +++ b/UnitystationLauncher/Services/AssemblyTypeCheckerService.cs @@ -2,21 +2,18 @@ using System.Collections.Concurrent; using System.Collections.Generic; using System.Diagnostics; -using System.Diagnostics.CodeAnalysis; using System.IO; using System.Linq; -using System.Net.Http; -using System.Reflection; using System.Reflection.Metadata; using System.Reflection.PortableExecutable; using System.Threading.Tasks; -using ILVerify; -using MoreLinq; using UnitystationLauncher.ContentScanning; +using UnitystationLauncher.ContentScanning.Scanners; +using UnitystationLauncher.Infrastructure; using UnitystationLauncher.Models.ConfigFile; using UnitystationLauncher.Models.ContentScanning; using UnitystationLauncher.Models.ContentScanning.ScanningTypes; -using UnitystationLauncher.Models.Enums; + using UnitystationLauncher.Services.Interface; // psst @@ -31,33 +28,13 @@ namespace UnitystationLauncher.Services; /// /// Manages the type white/black list of types and namespaces, and verifies assemblies against them. /// -public sealed partial class AssemblyTypeCheckerService : IAssemblyTypeCheckerService +public sealed class AssemblyTypeCheckerService : IAssemblyTypeCheckerService { - /// - /// Completely disables type checking, allowing everything. - /// - public bool DisableTypeCheck { get; init; } - - public DumpFlags Dump { get; init; } = DumpFlags.None; - public bool VerifyIl { get; init; } - private readonly Task _config; - private readonly IEnvironmentService _environmentService; - - - private readonly ICodeScanConfigService _codeScanConfigService; - - private readonly HttpClient _httpClient; - - public AssemblyTypeCheckerService(IEnvironmentService environmentService, HttpClient httpClient, ICodeScanConfigService codeScanConfigService) + public AssemblyTypeCheckerService(ICodeScanConfigService codeScanConfigService) { - _environmentService = environmentService; - VerifyIl = true; - DisableTypeCheck = false; - _httpClient = httpClient; - _codeScanConfigService = codeScanConfigService; - _config = Task.Run(_codeScanConfigService.LoadConfigAsync); + _config = codeScanConfigService.LoadConfigAsync(); } /// @@ -67,54 +44,45 @@ public AssemblyTypeCheckerService(IEnvironmentService environmentService, HttpCl /// /// /// - /// - /// - /// Assembly to load. + /// + /// /// - public bool CheckAssembly(FileInfo diskPath, DirectoryInfo managedPath, List otherAssemblies, - Action info, Action Errors) + public async Task CheckAssemblyTypesAsync(FileInfo diskPath, DirectoryInfo managedPath, List otherAssemblies, + Action infoAction, Action errorsAction) { - using FileStream assembly = diskPath.OpenRead(); + await using FileStream assembly = diskPath.OpenRead(); Stopwatch fullStopwatch = Stopwatch.StartNew(); Resolver resolver = AssemblyTypeCheckerHelpers.CreateResolver(managedPath); - using PEReader peReader = new PEReader(assembly, PEStreamOptions.LeaveOpen); + using PEReader peReader = new(assembly, PEStreamOptions.LeaveOpen); MetadataReader reader = peReader.GetMetadataReader(); string asmName = reader.GetString(reader.GetAssemblyDefinition().Name); + // Check for native code if (peReader.PEHeaders.CorHeader?.ManagedNativeHeaderDirectory is { Size: not 0 }) { - Errors.Invoke($"Assembly {asmName} contains native code."); + errorsAction.Invoke($"Assembly {asmName} contains native code."); return false; } - if (VerifyIl) + // Verify the IL + if (ILScanner.IsILValid(asmName, resolver, peReader, reader, infoAction, errorsAction, await _config) == false) { - if (DoVerifyIL(asmName, resolver, peReader, reader, info, Errors) == false) - { - Errors.Invoke($"Assembly {asmName} Has invalid IL code"); - return false; - } + errorsAction.Invoke($"Assembly {asmName} Has invalid IL code"); + return false; } + ConcurrentBag errors = new(); - ConcurrentBag errors = new ConcurrentBag(); - - List types = AssemblyTypeCheckerHelpers.GetReferencedTypes(reader, errors); - List members = AssemblyTypeCheckerHelpers.GetReferencedMembers(reader, errors); - List<(MType type, MType parent, ArraySegment interfaceImpls)> inherited = GetExternalInheritedTypes(reader, errors); - info.Invoke($"References loaded... {fullStopwatch.ElapsedMilliseconds}ms"); - - if (DisableTypeCheck) - { - resolver.Dispose(); - peReader.Dispose(); - return true; - } - + // Load all the references + List types = reader.GetReferencedTypes(errors); + List members = reader.GetReferencedMembers(errors); + List<(MType type, MType parent, ArraySegment interfaceImpls)> inherited = reader.GetExternalInheritedTypes(errors); + infoAction.Invoke(errors.IsEmpty ? "No sandbox violations." : $"Total violations: {errors.Count}"); + infoAction.Invoke($"References loaded... {fullStopwatch.ElapsedMilliseconds}ms"); - SandboxConfig loadedConfig = _config.Result; + SandboxConfig loadedConfig = await _config; loadedConfig.MultiAssemblyOtherReferences.Clear(); loadedConfig.MultiAssemblyOtherReferences.AddRange(otherAssemblies); @@ -122,502 +90,41 @@ public bool CheckAssembly(FileInfo diskPath, DirectoryInfo managedPath, List type.IsTypeAccessAllowed(loadedConfig, out _) == false)) { - if (IsTypeAccessAllowed(loadedConfig, type, out _) == false) - { - errors.Add(new SandboxError($"Access to type not allowed: {type} asmName {asmName}")); - } + errors.Add(new($"Access to type not allowed: {type} asmName {asmName}")); } - info.Invoke($"Types... {fullStopwatch.ElapsedMilliseconds}ms"); + infoAction.Invoke(errors.IsEmpty ? "No sandbox violations." : $"Total violations: {errors.Count}"); + infoAction.Invoke($"Types... {fullStopwatch.ElapsedMilliseconds}ms"); - CheckInheritance(loadedConfig, inherited, errors); + InheritanceScanner.CheckInheritance(loadedConfig, inherited, errors); + infoAction.Invoke(errors.IsEmpty ? "No sandbox violations." : $"Total violations: {errors.Count}"); + infoAction.Invoke($"Inheritance... {fullStopwatch.ElapsedMilliseconds}ms"); - info.Invoke($"Inheritance... {fullStopwatch.ElapsedMilliseconds}ms"); + UnmanagedMethodScanner.CheckNoUnmanagedMethodDefs(reader, errors); + infoAction.Invoke(errors.IsEmpty ? "No sandbox violations." : $"Total violations: {errors.Count}"); + infoAction.Invoke($"Unmanaged methods... {fullStopwatch.ElapsedMilliseconds}ms"); - AssemblyTypeCheckerHelpers.CheckNoUnmanagedMethodDefs(reader, errors); + TypeAbuseScanner.CheckNoTypeAbuse(reader, errors); + infoAction.Invoke(errors.IsEmpty ? "No sandbox violations." : $"Total violations: {errors.Count}"); + infoAction.Invoke($"Type abuse... {fullStopwatch.ElapsedMilliseconds}ms"); - info.Invoke($"Unmanaged methods... {fullStopwatch.ElapsedMilliseconds}ms"); + MemberReferenceScanner.CheckMemberReferences(loadedConfig, members, errors); + infoAction.Invoke(errors.IsEmpty ? "No sandbox violations." : $"Total violations: {errors.Count}"); + infoAction.Invoke($"Member References... {fullStopwatch.ElapsedMilliseconds}ms"); - AssemblyTypeCheckerHelpers.CheckNoTypeAbuse(reader, errors); - - info.Invoke($"Type abuse... {fullStopwatch.ElapsedMilliseconds}ms"); - - CheckMemberReferences(loadedConfig, members, errors); - - errors = new ConcurrentBag(errors.OrderBy(x => x.Message)); + errors = new(errors.OrderBy(x => x.Message)); foreach (SandboxError error in errors) { - Errors.Invoke($"Sandbox violation: {error.Message}"); + errorsAction.Invoke($"Sandbox violation: {error.Message}"); } - info.Invoke($"Checked assembly in {fullStopwatch.ElapsedMilliseconds}ms"); + infoAction.Invoke(errors.IsEmpty ? "No sandbox violations." : $"Total violations: {errors.Count}"); + infoAction.Invoke($"Checked assembly in {fullStopwatch.ElapsedMilliseconds}ms"); resolver.Dispose(); peReader.Dispose(); return errors.IsEmpty; } - - private bool DoVerifyIL( - string name, - IResolver resolver, - PEReader peReader, - MetadataReader reader, - Action info, - Action logErrors) - { - info.Invoke($"{name}: Verifying IL..."); - Stopwatch sw = Stopwatch.StartNew(); - ConcurrentBag bag = new ConcurrentBag(); - - - bool UesParallel = false; - - if (UesParallel) - { - OrderablePartitioner partitioner = Partitioner.Create(reader.TypeDefinitions); - Parallel.ForEach(partitioner.GetPartitions(Environment.ProcessorCount), handle => - { - Verifier ver = new Verifier(resolver); - ver.SetSystemModuleName(new AssemblyName(AssemblyTypeCheckerHelpers.SystemAssemblyName)); - while (handle.MoveNext()) - { - foreach (VerificationResult? result in ver.Verify(peReader, handle.Current, verifyMethods: true)) - { - bag.Add(result); - } - } - }); - } - else - { - Verifier ver = new Verifier(resolver); - //mscorlib - ver.SetSystemModuleName(new AssemblyName(AssemblyTypeCheckerHelpers.SystemAssemblyName)); - foreach (TypeDefinitionHandle Definition in reader.TypeDefinitions) - { - IEnumerable Errors = ver.Verify(peReader, Definition, verifyMethods: true); - foreach (VerificationResult? Error in Errors) - { - bag.Add(Error); - } - } - } - - SandboxConfig loadedCfg = _config.Result; - - bool verifyErrors = false; - foreach (VerificationResult res in bag) - { - if (loadedCfg.AllowedVerifierErrors.Contains(res.Code)) - { - continue; - } - - string formatted = res.Args == null ? res.Message : string.Format(res.Message, res.Args); - string msg = $"{name}: ILVerify: {formatted}"; - - if (!res.Method.IsNil) - { - MethodDefinition method = reader.GetMethodDefinition(res.Method); - string methodName = AssemblyTypeCheckerHelpers.FormatMethodName(reader, method); - - msg = $"{msg}, method: {methodName}"; - } - - if (!res.Type.IsNil) - { - MTypeDefined type = AssemblyTypeCheckerHelpers.GetTypeFromDefinition(reader, res.Type); - msg = $"{msg}, type: {type}"; - } - - - verifyErrors = true; - logErrors.Invoke(msg); - } - - info.Invoke($"{name}: Verified IL in {sw.Elapsed.TotalMilliseconds}ms"); - - if (verifyErrors) - { - return false; - } - - return true; - } - - private void CheckMemberReferences( - SandboxConfig sandboxConfig, - List members, - ConcurrentBag errors) - { - bool IsParallel = true; - - if (IsParallel) - { - Parallel.ForEach(members, memberRef => - { - MType baseType = memberRef.ParentType; - while (!(baseType is MTypeReferenced)) - { - switch (baseType) - { - case MTypeGeneric generic: - { - baseType = generic.GenericType; - - break; - } - case MTypeWackyArray: - { - // Members on arrays (not to be confused with vectors) are all fine. - // See II.14.2 in ECMA-335. - return; - } - case MTypeDefined: - { - // Valid for this to show up, safe to ignore. - return; - } - default: - { - throw new ArgumentOutOfRangeException(); - } - } - } - - MTypeReferenced baseTypeReferenced = (MTypeReferenced)baseType; - - if (IsTypeAccessAllowed(sandboxConfig, baseTypeReferenced, out TypeConfig? typeCfg) == false) - { - // Technically this error isn't necessary since we have an earlier pass - // checking all referenced types. That should have caught this - // We still need the typeCfg so that's why we're checking. Might as well. - errors.Add(new SandboxError($"Access to type not allowed: {baseTypeReferenced}")); - return; - } - - if (typeCfg.All) - { - // Fully whitelisted for the type, we good. - return; - } - - switch (memberRef) - { - case MMemberRefField mMemberRefField: - { - foreach (WhitelistFieldDefine field in typeCfg.FieldsParsed) - { - if (field.Name == mMemberRefField.Name && - mMemberRefField.FieldType.WhitelistEquals(field.FieldType)) - { - return; // Found - } - } - - errors.Add(new SandboxError($"Access to field not allowed: {mMemberRefField}")); - break; - } - case MMemberRefMethod mMemberRefMethod: - foreach (WhitelistMethodDefine parsed in typeCfg.MethodsParsed) - { - bool notParamMismatch = true; - - if (parsed.Name == mMemberRefMethod.Name && - mMemberRefMethod.ReturnType.WhitelistEquals(parsed.ReturnType) && - mMemberRefMethod.ParameterTypes.Length == parsed.ParameterTypes.Count && - mMemberRefMethod.GenericParameterCount == parsed.GenericParameterCount) - { - for (int i = 0; i < mMemberRefMethod.ParameterTypes.Length; i++) - { - MType a = mMemberRefMethod.ParameterTypes[i]; - MType b = parsed.ParameterTypes[i]; - - if (a.WhitelistEquals(b) == false) - { - notParamMismatch = false; - break; - } - } - - if (notParamMismatch) - { - return; // Found - } - } - } - - errors.Add(new SandboxError($"Access to method not allowed: {mMemberRefMethod}")); - break; - default: - throw new ArgumentOutOfRangeException(nameof(memberRef)); - } - }); - } - else - { - foreach (MMemberRef memberRef in members) - { - MType baseType = memberRef.ParentType; - while (!(baseType is MTypeReferenced)) - { - switch (baseType) - { - case MTypeGeneric generic: - { - baseType = generic.GenericType; - - break; - } - case MTypeWackyArray: - { - // Members on arrays (not to be confused with vectors) are all fine. - // See II.14.2 in ECMA-335. - continue; - } - case MTypeDefined: - { - // Valid for this to show up, safe to ignore. - continue; - } - default: - { - throw new ArgumentOutOfRangeException(); - } - } - } - - MTypeReferenced baseTypeReferenced = (MTypeReferenced)baseType; - - if (IsTypeAccessAllowed(sandboxConfig, baseTypeReferenced, out TypeConfig? typeCfg) == false) - { - // Technically this error isn't necessary since we have an earlier pass - // checking all referenced types. That should have caught this - // We still need the typeCfg so that's why we're checking. Might as well. - errors.Add(new SandboxError($"Access to type not allowed: {baseTypeReferenced}")); - continue; - } - - if (typeCfg.All) - { - // Fully whitelisted for the type, we good. - continue; - } - - switch (memberRef) - { - case MMemberRefField mMemberRefField: - { - foreach (WhitelistFieldDefine field in typeCfg.FieldsParsed) - { - if (field.Name == mMemberRefField.Name && - mMemberRefField.FieldType.WhitelistEquals(field.FieldType)) - { - continue; // Found - } - } - - errors.Add(new SandboxError($"Access to field not allowed: {mMemberRefField}")); - break; - } - case MMemberRefMethod mMemberRefMethod: - bool notParamMismatch = true; - foreach (WhitelistMethodDefine parsed in typeCfg.MethodsParsed) - { - if (parsed.Name == mMemberRefMethod.Name && - mMemberRefMethod.ReturnType.WhitelistEquals(parsed.ReturnType) && - mMemberRefMethod.ParameterTypes.Length == parsed.ParameterTypes.Count && - mMemberRefMethod.GenericParameterCount == parsed.GenericParameterCount) - { - for (int i = 0; i < mMemberRefMethod.ParameterTypes.Length; i++) - { - MType a = mMemberRefMethod.ParameterTypes[i]; - MType b = parsed.ParameterTypes[i]; - - if (!a.WhitelistEquals(b)) - { - notParamMismatch = false; - break; - - } - } - - if (notParamMismatch) - { - break; // Found - } - break; - } - } - - if (notParamMismatch == false) - { - continue; - } - - errors.Add(new SandboxError($"Access to method not allowed: {mMemberRefMethod}")); - break; - default: - throw new ArgumentOutOfRangeException(nameof(memberRef)); - } - } - } - } - - private void CheckInheritance( - SandboxConfig sandboxConfig, - List<(MType type, MType parent, ArraySegment interfaceImpls)> inherited, - ConcurrentBag errors) - { - // This inheritance whitelisting primarily serves to avoid content doing funny stuff - // by e.g. inheriting Type. - foreach ((MType _, MType baseType, ArraySegment interfaces) in inherited) - { - if (CanInherit(baseType) == false) - { - errors.Add(new SandboxError($"Inheriting of type not allowed: {baseType}")); - } - - foreach (MType @interface in interfaces) - { - if (CanInherit(@interface) == false) - { - errors.Add(new SandboxError($"Implementing of interface not allowed: {@interface}")); - } - } - - bool CanInherit(MType inheritType) - { - MTypeReferenced realBaseType = inheritType switch - { - MTypeGeneric generic => (MTypeReferenced)generic.GenericType, - MTypeReferenced referenced => referenced, - _ => throw new InvalidOperationException() // Can't happen. - }; - - if (IsTypeAccessAllowed(sandboxConfig, realBaseType, out TypeConfig? cfg) == false) - { - return false; - } - - return cfg.Inherit != InheritMode.Block && (cfg.Inherit == InheritMode.Allow || cfg.All); - } - } - } - - private bool IsTypeAccessAllowed(SandboxConfig sandboxConfig, MTypeReferenced type, - [NotNullWhen(true)] out TypeConfig? cfg) - { - if (type.Namespace == null) - { - if (type.ResolutionScope is MResScopeType parentType) - { - if (IsTypeAccessAllowed(sandboxConfig, (MTypeReferenced)parentType.Type, out TypeConfig? parentCfg) == false) - { - cfg = null; - return false; - } - - if (parentCfg.All) - { - // Enclosing type is namespace-whitelisted so we don't have to check anything else. - cfg = TypeConfig.DefaultAll; - return true; - } - - // Found enclosing type, checking if we are allowed to access this nested type. - // Also pass it up in case of multiple nested types. - if (parentCfg.NestedTypes != null && parentCfg.NestedTypes.TryGetValue(type.Name, out cfg)) - { - return true; - } - - cfg = null; - return false; - } - - if (type.ResolutionScope is MResScopeAssembly mResScopeAssembly && - sandboxConfig.MultiAssemblyOtherReferences.Contains(mResScopeAssembly.Name)) - { - cfg = TypeConfig.DefaultAll; - return true; - } - - // Types without namespaces or nesting parent are not allowed at all. - cfg = null; - return false; - } - - // Check if in whitelisted namespaces. - foreach (string whNamespace in sandboxConfig.WhitelistedNamespaces) - { - if (type.Namespace.StartsWith(whNamespace)) - { - cfg = TypeConfig.DefaultAll; - return true; - } - } - - if (type.ResolutionScope is MResScopeAssembly resScopeAssembly && - sandboxConfig.MultiAssemblyOtherReferences.Contains(resScopeAssembly.Name)) - { - cfg = TypeConfig.DefaultAll; - return true; - } - - - if (sandboxConfig.Types.TryGetValue(type.Namespace, out Dictionary? nsDict) == false) - { - cfg = null; - return false; - } - - return nsDict.TryGetValue(type.Name, out cfg); - } - - private List<(MType type, MType parent, ArraySegment interfaceImpls)> GetExternalInheritedTypes( - MetadataReader reader, - ConcurrentBag errors) - { - List<(MType, MType, ArraySegment)> list = new List<(MType, MType, ArraySegment)>(); - foreach (TypeDefinitionHandle typeDefHandle in reader.TypeDefinitions) - { - TypeDefinition typeDef = reader.GetTypeDefinition(typeDefHandle); - ArraySegment interfaceImpls; - MTypeDefined type = AssemblyTypeCheckerHelpers.GetTypeFromDefinition(reader, typeDefHandle); - - if (!AssemblyTypeCheckerHelpers.ParseInheritType(type, typeDef.BaseType, out MType? parent, reader, errors)) - { - continue; - } - - InterfaceImplementationHandleCollection interfaceImplsCollection = typeDef.GetInterfaceImplementations(); - if (interfaceImplsCollection.Count == 0) - { - interfaceImpls = Array.Empty(); - } - else - { - interfaceImpls = new MType[interfaceImplsCollection.Count]; - int i = 0; - foreach (InterfaceImplementationHandle implHandle in interfaceImplsCollection) - { - InterfaceImplementation interfaceImpl = reader.GetInterfaceImplementation(implHandle); - - if (AssemblyTypeCheckerHelpers.ParseInheritType(type, interfaceImpl.Interface, out MType? implemented, reader, errors)) - { - interfaceImpls[i++] = implemented; - } - } - - interfaceImpls = interfaceImpls[..i]; - } - - list.Add((type, parent, interfaceImpls)); - } - - return list; - } } \ No newline at end of file diff --git a/UnitystationLauncher/Services/BlogService.cs b/UnitystationLauncher/Services/BlogService.cs index af5e245b..118b54d2 100644 --- a/UnitystationLauncher/Services/BlogService.cs +++ b/UnitystationLauncher/Services/BlogService.cs @@ -2,9 +2,6 @@ using System.Linq; using System.Net.Http; using System.Text.Json; -using System.Text.Json.Serialization; -using System.Threading.Tasks; -using Avalonia.Media.Imaging; using UnitystationLauncher.Models.Api.Changelog; using UnitystationLauncher.Services.Interface; diff --git a/UnitystationLauncher/Services/CodeScanConfigService.cs b/UnitystationLauncher/Services/CodeScanConfigService.cs index 190168af..348c8102 100644 --- a/UnitystationLauncher/Services/CodeScanConfigService.cs +++ b/UnitystationLauncher/Services/CodeScanConfigService.cs @@ -10,7 +10,9 @@ using System.Threading.Tasks; using Pidgin; using Serilog; +using UnitystationLauncher.Constants; using UnitystationLauncher.ContentScanning; +using UnitystationLauncher.Exceptions; using UnitystationLauncher.Models.ConfigFile; using UnitystationLauncher.Models.ContentScanning; using UnitystationLauncher.Models.Enums; @@ -20,15 +22,13 @@ namespace UnitystationLauncher.Services; public class CodeScanConfigService : ICodeScanConfigService { - private static string NameConfig = @"CodeScanList.json"; + private static string _nameConfig = @"CodeScanList.json"; private readonly HttpClient _httpClient; private readonly IPreferencesService _preferencesService; private readonly IEnvironmentService _environmentService; - private const string GoodFileURL = "Https://unitystationfile.b-cdn.net/GoodFiles/"; - public CodeScanConfigService(HttpClient httpClient, IPreferencesService preferencesService, IEnvironmentService environmentService) { _httpClient = httpClient; @@ -37,9 +37,10 @@ public CodeScanConfigService(HttpClient httpClient, IPreferencesService preferen } - public async Task<(string, bool)> GetGoodFileVersion(string version) + #region Public Interface + public async Task<(string, bool)> GetGoodFileVersionAsync(string version) { - if (await ValidGoodFilesVersion(version) == false) + if (await ValidGoodFilesVersionAsync(version) == false) { return ("", false); } @@ -50,60 +51,25 @@ public CodeScanConfigService(HttpClient httpClient, IPreferencesService preferen if (Directory.Exists(versionPath) == false) { - string ZIPExtractPath = Path.Combine(pathBase, version); - HttpResponseMessage request = await _httpClient.GetAsync(GoodFileURL + version + "/" + folderName + ".zip", HttpCompletionOption.ResponseHeadersRead); + string zipExtractPath = Path.Combine(pathBase, version); + HttpResponseMessage request = await _httpClient.GetAsync($"{ApiUrls.GoodFilesBaseUrl}/{version}/{folderName}.zip", HttpCompletionOption.ResponseHeadersRead); await using Stream responseStream = await request.Content.ReadAsStreamAsync(); ZipArchive archive = new(responseStream); - archive.ExtractToDirectory(ZIPExtractPath, true); + archive.ExtractToDirectory(zipExtractPath, true); - string ZIPDirectory = Path.Combine(ZIPExtractPath, GetZipFolderName()); - Directory.Move(ZIPDirectory, versionPath); + string zipDirectory = Path.Combine(zipExtractPath, GetZipFolderName()); + Directory.Move(zipDirectory, versionPath); } return (versionPath, true); } - - private string GetZipFolderName() - { - CurrentEnvironment OS = _environmentService.GetCurrentEnvironment(); - switch (OS) - { - case CurrentEnvironment.WindowsStandalone: - return "Windows"; - case CurrentEnvironment.LinuxFlatpak: - case CurrentEnvironment.LinuxStandalone: - return "Linux"; - case CurrentEnvironment.MacOsStandalone: - return "Mac"; - default: - throw new Exception($"Unable to determine OS Version {OS}"); - } - } - - private string GetFolderName(string version) - { - CurrentEnvironment OS = _environmentService.GetCurrentEnvironment(); - switch (OS) - { - case CurrentEnvironment.WindowsStandalone: - return version + "_Windows"; - case CurrentEnvironment.LinuxFlatpak: - case CurrentEnvironment.LinuxStandalone: - return version + "_Linux"; - case CurrentEnvironment.MacOsStandalone: - return version + "_Mac"; - default: - throw new Exception($"Unable to determine OS Version {OS}"); - } - } - - public async Task ValidGoodFilesVersion(string goodFileVersion) + public async Task ValidGoodFilesVersionAsync(string goodFileVersion) { string jsonData = ""; try { - HttpResponseMessage response = await _httpClient.GetAsync("https://unitystationfile.b-cdn.net/GoodFiles/AllowGoodFiles.json"); + HttpResponseMessage response = await _httpClient.GetAsync(ApiUrls.AllowedGoodFilesUrl); if (!response.IsSuccessStatusCode) { Log.Error("Unable to download config" + response); @@ -114,7 +80,7 @@ public async Task ValidGoodFilesVersion(string goodFileVersion) } catch (Exception e) { - Log.Error("Unable to download ValidGoodFilesVersion config" + e); + Log.Error("Unable to download ValidGoodFilesVersionAsync config" + e); return false; } @@ -133,93 +99,115 @@ public async Task ValidGoodFilesVersion(string goodFileVersion) return allowedList.Contains(goodFileVersion); } - public string SanitiseStringPath(string inString) - { - return inString.Replace(@"\", "").Replace("/", "").Replace(".", "_"); - } - - private static bool TryDownloadVersion() - { - return false; - } - public async Task LoadConfigAsync() { - string configPath = Path.Combine(_environmentService.GetUserdataDirectory(), NameConfig); + string configPath = Path.Combine(_environmentService.GetUserdataDirectory(), _nameConfig); try { - HttpResponseMessage response = await _httpClient.GetAsync("https://raw.githubusercontent.com/unitystation/unitystation/develop/CodeScanList.json"); + HttpResponseMessage response = await _httpClient.GetAsync(ApiUrls.CodeScanListUrl); if (response.IsSuccessStatusCode) { string jsonData = await response.Content.ReadAsStringAsync(); File.Delete(configPath); await File.WriteAllTextAsync(configPath, jsonData); - Console.WriteLine("JSON file saved successfully."); + Log.Information("JSON file saved successfully."); } else { - Log.Error("Unable to download config" + response.ToString()); + Log.Error("Unable to download config" + response); } } catch (Exception e) { - Log.Error("Unable to download config" + e.ToString()); + Log.Error("Unable to download config" + e); } - if (Exists(configPath) == false) + if (File.Exists(configPath) == false) { Assembly assembly = Assembly.GetExecutingAssembly(); string resourceName = "UnitystationLauncher.CodeScanList.json"; - using (Stream? stream = assembly.GetManifestResourceStream(resourceName)) + await using (Stream? stream = assembly.GetManifestResourceStream(resourceName)) { if (stream != null) { // Copy the contents of the resource to a file location - using (FileStream fileStream = File.Create(configPath)) - { - stream.Seek(0L, SeekOrigin.Begin); - await stream.CopyToAsync(fileStream); - } + await using FileStream fileStream = File.Create(configPath); + stream.Seek(0L, SeekOrigin.Begin); + await stream.CopyToAsync(fileStream); } } Log.Error("had to use backup config"); } - using (StreamReader file = OpenText(configPath)) + using StreamReader file = File.OpenText(configPath); + try { - try + SandboxConfig? data = JsonSerializer.Deserialize(await file.ReadToEndAsync(), new JsonSerializerOptions { - SandboxConfig? data = JsonSerializer.Deserialize(file.ReadToEnd(), new JsonSerializerOptions - { - AllowTrailingCommas = true, - Converters = - { - new JsonStringEnumConverter(allowIntegerValues: false) - } - }); - - if (data == null) - { - Log.Error("unable to de-serialise config"); - throw new DataException("unable to de-serialise config"); - } - - foreach (KeyValuePair> @namespace in data.Types) + AllowTrailingCommas = true, + Converters = { - foreach (KeyValuePair @class in @namespace.Value) - { - ParseTypeConfig(@class.Value); - } + new JsonStringEnumConverter(allowIntegerValues: false) } + }); - return data; + if (data == null) + { + Log.Error("unable to de-serialise config"); + throw new DataException("unable to de-serialise config"); } - catch (Exception e) + + foreach (KeyValuePair> @namespace in data.Types) { - Console.WriteLine(e); - throw; + foreach (KeyValuePair @class in @namespace.Value) + { + ParseTypeConfig(@class.Value); + } } + + return data; + } + catch (Exception e) + { + Log.Error(e, e.Message); + throw; + } + } + #endregion + + #region Private Helpers + private string GetZipFolderName() + { + CurrentEnvironment os = _environmentService.GetCurrentEnvironment(); + switch (os) + { + case CurrentEnvironment.WindowsStandalone: + return "Windows"; + case CurrentEnvironment.LinuxFlatpak: + case CurrentEnvironment.LinuxStandalone: + return "Linux"; + case CurrentEnvironment.MacOsStandalone: + return "Mac"; + default: + throw new UnsupportedPlatformException($"Unable to determine OS Version {os}"); + } + } + + private string GetFolderName(string version) + { + CurrentEnvironment os = _environmentService.GetCurrentEnvironment(); + switch (os) + { + case CurrentEnvironment.WindowsStandalone: + return version + "_Windows"; + case CurrentEnvironment.LinuxFlatpak: + case CurrentEnvironment.LinuxStandalone: + return version + "_Linux"; + case CurrentEnvironment.MacOsStandalone: + return version + "_Mac"; + default: + throw new UnsupportedPlatformException($"Unable to determine OS Version {os}"); } } @@ -227,7 +215,7 @@ private static void ParseTypeConfig(TypeConfig cfg) { if (cfg.Methods != null) { - List list = new List(); + List list = new(); foreach (string m in cfg.Methods) { try @@ -249,7 +237,7 @@ private static void ParseTypeConfig(TypeConfig cfg) if (cfg.Fields != null) { - List list = new List(); + List list = new(); foreach (string f in cfg.Fields) { try @@ -278,14 +266,5 @@ private static void ParseTypeConfig(TypeConfig cfg) } } } - - public StreamReader OpenText(string path) - { - return File.OpenText(path); - } - - public bool Exists(string path) - { - return File.Exists(path); - } + #endregion } \ No newline at end of file diff --git a/UnitystationLauncher/Services/CodeScanService.cs b/UnitystationLauncher/Services/CodeScanService.cs index 51370ef8..f64313be 100644 --- a/UnitystationLauncher/Services/CodeScanService.cs +++ b/UnitystationLauncher/Services/CodeScanService.cs @@ -5,6 +5,8 @@ using System.Linq; using System.Threading.Tasks; using Serilog; +using UnitystationLauncher.Constants; +using UnitystationLauncher.Exceptions; using UnitystationLauncher.Models.Enums; using UnitystationLauncher.Services.Interface; @@ -12,32 +14,26 @@ namespace UnitystationLauncher.Services; public class CodeScanService : ICodeScanService { - private readonly IAssemblyTypeCheckerService _IAssemblyChecker; + private readonly IAssemblyTypeCheckerService _assemblyTypeCheckerService; private readonly IEnvironmentService _environmentService; - private readonly ICodeScanConfigService _iGoodFileService; + private readonly ICodeScanConfigService _codeScanConfigService; private readonly IPreferencesService _preferencesService; - private const string Managed = "Managed"; - private const string Plugins = "Plugins"; - private const string Unitystation_Data = "Unitystation_Data"; - public CodeScanService(IAssemblyTypeCheckerService assemblyChecker, IEnvironmentService environmentService, ICodeScanConfigService iGoodFileService, - IPreferencesService ipreferencesService) + + public CodeScanService(IAssemblyTypeCheckerService assemblyTypeCheckerService, IEnvironmentService environmentService, ICodeScanConfigService codeScanConfigService, + IPreferencesService preferencesService) { - _IAssemblyChecker = assemblyChecker; + _assemblyTypeCheckerService = assemblyTypeCheckerService; _environmentService = environmentService; - _iGoodFileService = iGoodFileService; - _preferencesService = ipreferencesService; + _codeScanConfigService = codeScanConfigService; + _preferencesService = preferencesService; } - - - - - public async Task OnScan(ZipArchive archive, string targetDirectory, string goodFileVersion, Action info, Action errors) + public async Task OnScanAsync(ZipArchive archive, string targetDirectory, string goodFileVersion, Action info, Action errors) { // TODO: Enable extraction cancelling - DirectoryInfo root = new DirectoryInfo(_preferencesService.GetPreferences().InstallationPath); + DirectoryInfo root = new(_preferencesService.GetPreferences().InstallationPath); DirectoryInfo stagingDirectory = root.CreateSubdirectory("UnsafeBuildZipDirectory"); DirectoryInfo processingDirectory = root.CreateSubdirectory("UnsafeBuildProcessing"); @@ -61,8 +57,6 @@ public async Task OnScan(ZipArchive archive, string targetDirectory, strin DeleteFilesWithExtension(processingDirectory.ToString(), ".bundle", exceptionDirectory: Path.Combine(processingDirectory.ToString(), @"Unitystation.app/Contents/Resources/Data/StreamingAssets")); } - - DirectoryInfo? stagingManaged = null; if (_environmentService.GetCurrentEnvironment() != CurrentEnvironment.MacOsStandalone) { @@ -88,22 +82,22 @@ public async Task OnScan(ZipArchive archive, string targetDirectory, strin errors.Invoke("oh God NO Datapath Exiting!!!"); return false; } - stagingManaged = stagingDirectory.CreateSubdirectory(Path.Combine(dataPath.Name, Managed)); + stagingManaged = stagingDirectory.CreateSubdirectory(Path.Combine(dataPath.Name, FolderNames.Managed)); } else { //MAC - dataPath = new DirectoryInfo(Path.Combine(processingDirectory.ToString(), @"Unitystation.app/Contents/Resources/Data")); - stagingManaged = stagingDirectory.CreateSubdirectory(Path.Combine(@"Unitystation.app/Contents/Resources/Data", Managed)); + dataPath = new(Path.Combine(processingDirectory.ToString(), @"Unitystation.app/Contents/Resources/Data")); + stagingManaged = stagingDirectory.CreateSubdirectory(Path.Combine(@"Unitystation.app/Contents/Resources/Data", FolderNames.Managed)); } - DirectoryInfo dllDirectory = dataPath.CreateSubdirectory(Managed); + DirectoryInfo dllDirectory = dataPath.CreateSubdirectory(FolderNames.Managed); CopyFilesRecursively(stagingManaged.ToString(), dllDirectory.ToString()); - (string goodFilePath, bool booly) = await _iGoodFileService.GetGoodFileVersion(goodFileVersion); + (string goodFilePath, bool booly) = await _codeScanConfigService.GetGoodFileVersionAsync(goodFileVersion); if (booly == false) { @@ -112,10 +106,10 @@ public async Task OnScan(ZipArchive archive, string targetDirectory, strin return false; } - DirectoryInfo goodFileCopy = new DirectoryInfo(GetManagedOnOS(goodFilePath)); + DirectoryInfo goodFileCopy = new(GetManagedOnOS(goodFilePath)); info.Invoke("Proceeding to scan folder"); - if (ScanFolder(dllDirectory, goodFileCopy, info, errors) == false) + if (await ScanFolderAsync(dllDirectory, goodFileCopy, info, errors) == false) { try { @@ -129,56 +123,29 @@ public async Task OnScan(ZipArchive archive, string targetDirectory, strin return false; } - - CopyFilesRecursively(goodFilePath, processingDirectory.ToString()); - if (dataPath.Name != Unitystation_Data && _environmentService.GetCurrentEnvironment() != CurrentEnvironment.MacOsStandalone) //I know Cases and to file systems but F + if (dataPath.Name != FolderNames.UnitystationData && _environmentService.GetCurrentEnvironment() != CurrentEnvironment.MacOsStandalone) //I know Cases and to file systems but F { - string oldPath = Path.Combine(processingDirectory.ToString(), Unitystation_Data); + string oldPath = Path.Combine(processingDirectory.ToString(), FolderNames.UnitystationData); CopyFilesRecursively(oldPath, dataPath.ToString()); Directory.Delete(oldPath, true); } - switch (_environmentService.GetCurrentEnvironment()) { case CurrentEnvironment.WindowsStandalone: - FileInfo? exeRename = processingDirectory.GetFiles() - .FirstOrDefault(x => x.Extension == ".exe" && x.Name != "UnityCrashHandler64.exe"); //TODO OS - - - if (exeRename == null || exeRename.Directory == null) - { - errors.Invoke("no Executable found "); - DeleteContentsOfDirectory(processingDirectory); - DeleteContentsOfDirectory(stagingDirectory); - return false; - } - info.Invoke($"Found exeRename {exeRename}"); - exeRename.MoveTo(Path.Combine(exeRename.Directory.ToString(), dataPath.Name.Replace("_Data", "") + ".exe")); + LocateWindowsExecutable(processingDirectory, stagingDirectory, dataPath, info, errors); break; case CurrentEnvironment.LinuxFlatpak: case CurrentEnvironment.LinuxStandalone: - FileInfo? ExecutableRename = processingDirectory.GetFiles() - .FirstOrDefault(x => x.Extension == ""); - - if (ExecutableRename == null || ExecutableRename.Directory == null) - { - errors.Invoke("no Executable found "); - DeleteContentsOfDirectory(processingDirectory); - DeleteContentsOfDirectory(stagingDirectory); - return false; - } - info.Invoke($"Found ExecutableRename {ExecutableRename}"); - ExecutableRename.MoveTo(Path.Combine(ExecutableRename.Directory.ToString(), dataPath.Name.Replace("_Data", "") + "")); + LocateLinuxExecutable(processingDirectory, stagingDirectory, dataPath, info, errors); break; } - - DirectoryInfo targetDirectoryinfo = new DirectoryInfo(targetDirectory); - if (targetDirectoryinfo.Exists) + DirectoryInfo targetDirectoryInfo = new(targetDirectory); + if (targetDirectoryInfo.Exists) { - DeleteContentsOfDirectory(targetDirectoryinfo); + DeleteContentsOfDirectory(targetDirectoryInfo); } CopyFilesRecursively(processingDirectory.ToString(), targetDirectory.ToString()); @@ -196,26 +163,61 @@ public async Task OnScan(ZipArchive archive, string targetDirectory, strin return true; } - public string GetManagedOnOS(string GoodFiles) + private static void LocateWindowsExecutable(DirectoryInfo processingDirectory, DirectoryInfo stagingDirectory, DirectoryInfo dataPath, Action info, Action errors) + { + FileInfo? exeRename = processingDirectory.GetFiles() + .FirstOrDefault(x => x.Extension == ".exe" && x.Name != "UnityCrashHandler64.exe"); //TODO OS + + + if (exeRename?.Directory == null) + { + errors.Invoke("no Executable found "); + DeleteContentsOfDirectory(processingDirectory); + DeleteContentsOfDirectory(stagingDirectory); + throw new CodeScanningException("No Windows executable found"); + } + + info.Invoke($"Found exeRename {exeRename}"); + exeRename.MoveTo(Path.Combine(exeRename.Directory.ToString(), dataPath.Name.Replace("_Data", "") + ".exe")); + } + + private static void LocateLinuxExecutable(DirectoryInfo processingDirectory, DirectoryInfo stagingDirectory, DirectoryInfo dataPath, Action info, Action errors) + { + FileInfo? executableRename = processingDirectory.GetFiles() + .FirstOrDefault(x => x.Extension == ""); + + if (executableRename?.Directory == null) + { + errors.Invoke("no Executable found "); + DeleteContentsOfDirectory(processingDirectory); + DeleteContentsOfDirectory(stagingDirectory); + throw new CodeScanningException("No Linux executable found"); + } + + info.Invoke($"Found ExecutableRename {executableRename}"); + executableRename.MoveTo(Path.Combine(executableRename.Directory.ToString(), dataPath.Name.Replace("_Data", "") + "")); + } + + private string GetManagedOnOS(string goodFiles) { - CurrentEnvironment OS = _environmentService.GetCurrentEnvironment(); - switch (OS) + CurrentEnvironment os = _environmentService.GetCurrentEnvironment(); + switch (os) { case CurrentEnvironment.WindowsStandalone: - return Path.Combine(GoodFiles, Unitystation_Data, Managed); + return Path.Combine(goodFiles, FolderNames.UnitystationData, FolderNames.Managed); case CurrentEnvironment.LinuxFlatpak: case CurrentEnvironment.LinuxStandalone: - return Path.Combine(GoodFiles, Unitystation_Data, Managed); + return Path.Combine(goodFiles, FolderNames.UnitystationData, FolderNames.Managed); case CurrentEnvironment.MacOsStandalone: - return Path.Combine(GoodFiles, @"Unitystation.app/Contents/Resources/Data", Managed); + return Path.Combine(goodFiles, @"Unitystation.app/Contents/Resources/Data", FolderNames.Managed); default: - throw new Exception($"Unable to determine OS Version {OS}"); + throw new($"Unable to determine OS Version {os}"); } } - public bool ScanFolder(DirectoryInfo @unsafe, DirectoryInfo saveFiles, Action info, Action errors) + private async Task ScanFolderAsync(DirectoryInfo @unsafe, DirectoryInfo saveFiles, Action info, Action errors) { List goodFiles = saveFiles.GetFiles().Select(x => x.Name).ToList(); @@ -226,7 +228,7 @@ public bool ScanFolder(DirectoryInfo @unsafe, DirectoryInfo saveFiles, Action multiAssemblyReference = new List(); + List multiAssemblyReference = new(); foreach (FileInfo file in files) { @@ -245,7 +247,7 @@ public bool ScanFolder(DirectoryInfo @unsafe, DirectoryInfo saveFiles, Action listy = multiAssemblyReference.ToList(); listy.Remove(Path.GetFileNameWithoutExtension(file.Name)); - if (_IAssemblyChecker.CheckAssembly(file, @unsafe, listy, info, errors) == false) + if (await _assemblyTypeCheckerService.CheckAssemblyTypesAsync(file, @unsafe, listy, info, errors) == false) { errors.Invoke($"{file.Name} Failed scanning Cancelling"); return false; @@ -284,12 +286,12 @@ public static void DeleteContentsOfDirectory(DirectoryInfo directory) static void DeleteFilesWithExtension(string directoryPath, string fileExtension, bool recursive = true, string? exceptionDirectory = null) { - DirectoryInfo directory = new DirectoryInfo(directoryPath); + DirectoryInfo directory = new(directoryPath); // Check if the directory exists if (!directory.Exists) { - Console.WriteLine("Directory not found: " + directoryPath); + Log.Error("Directory not found: " + directoryPath); return; } @@ -306,7 +308,7 @@ static void DeleteFilesWithExtension(string directoryPath, string fileExtension, } // Delete the file file.Delete(); - Console.WriteLine("Deleted file: " + file.FullName); + Log.Debug("Deleted file: " + file.FullName); } if (recursive) @@ -327,7 +329,7 @@ static void CopyFilesRecursively(string sourceDirectory, string destinationDirec // Create the destination directory if it doesn't exist Directory.CreateDirectory(destinationDirectory); - DirectoryInfo source = new DirectoryInfo(sourceDirectory); + DirectoryInfo source = new(sourceDirectory); // Get the files from the source directory FileInfo[] files = source.GetFiles(); diff --git a/UnitystationLauncher/Services/InstallationService.cs b/UnitystationLauncher/Services/InstallationService.cs index 1895ddd9..90040184 100644 --- a/UnitystationLauncher/Services/InstallationService.cs +++ b/UnitystationLauncher/Services/InstallationService.cs @@ -31,7 +31,7 @@ public class InstallationService : IInstallationService private readonly IServerService _serverService; private readonly ICodeScanService _codeScanService; - private readonly ICodeScanConfigService _iGoodFileService; + private readonly ICodeScanConfigService _codeScanConfigService; private readonly List _downloads; private List _installations = new(); @@ -39,14 +39,14 @@ public class InstallationService : IInstallationService public InstallationService(HttpClient httpClient, IPreferencesService preferencesService, IEnvironmentService environmentService, IServerService serverService, ICodeScanService codeScanService, - ICodeScanConfigService iGoodFileService) + ICodeScanConfigService codeScanConfigService) { _httpClient = httpClient; _preferencesService = preferencesService; _environmentService = environmentService; _serverService = serverService; _codeScanService = codeScanService; - _iGoodFileService = iGoodFileService; + _codeScanConfigService = codeScanConfigService; _downloads = new(); _installationsJsonFilePath = Path.Combine(_environmentService.GetUserdataDirectory(), "installations.json"); @@ -78,7 +78,7 @@ public List GetInstallations() && d.BuildVersion == buildVersion); } - public async Task<(Download?, string)> DownloadInstallation(Server server) + public async Task<(Download?, string)> DownloadInstallationAsync(Server server) { string? downloadUrl = server.GetDownloadUrl(_environmentService); if (string.IsNullOrWhiteSpace(downloadUrl)) @@ -90,7 +90,7 @@ public List GetInstallations() server.ServerGoodFileVersion = "1.0.0"; //TODO - bool result = await _iGoodFileService.ValidGoodFilesVersion(server.ServerGoodFileVersion); + bool result = await _codeScanConfigService.ValidGoodFilesVersionAsync(server.ServerGoodFileVersion); if (result == false) { @@ -110,11 +110,11 @@ public List GetInstallations() string installationBasePath = _preferencesService.GetPreferences().InstallationPath; // should be something like {basePath}/{forkName}/{version} - string installationPath = Path.Combine(installationBasePath, _iGoodFileService.SanitiseStringPath(server.ForkName), _iGoodFileService.SanitiseStringPath(server.ServerGoodFileVersion), server.BuildVersion.ToString()); + string installationPath = Path.Combine(installationBasePath, server.ForkName.SanitiseStringPath(), server.ServerGoodFileVersion.SanitiseStringPath(), server.BuildVersion.ToString()); download = new(downloadUrl, installationPath, server.ForkName, server.BuildVersion, server.ServerGoodFileVersion); - (bool canStartDownload, string cantDownloadReason) = InstallationService.CanStartDownload(download); + (bool canStartDownload, string cantDownloadReason) = CanStartDownload(download); if (!canStartDownload) { @@ -128,15 +128,10 @@ public List GetInstallations() } _downloads.Add(download); - download.Active = true; - RxApp.MainThreadScheduler.ScheduleAsync((_, _) => StartDownloadAsync(download)); return (download, string.Empty); } - - - public (bool, string) StartInstallation(Guid installationId, string? server = null, short? port = null) { Installation? installation = GetInstallationById(installationId); @@ -157,7 +152,7 @@ public List GetInstallations() EnsureExecutableFlagOnUnixSystems(executable); - string arguments = InstallationService.GetArguments(server, port); + string arguments = GetArguments(server, port); ProcessStartInfo? startInfo = _environmentService.GetGameProcessStartInfo(executable, arguments); if (startInfo == null) @@ -269,7 +264,7 @@ public bool MoveInstallations(string newBasePath) continue; } - InstallationService.CreateParentDirectory(newPath); + CreateParentDirectory(newPath); if (Directory.Exists(newPath)) { @@ -419,14 +414,16 @@ private static string GetArguments(string? server, long? port) return arguments; } - public static List InfoList = new List(); - public static List ErrorList = new List(); + private async Task StartDownloadAsync(Download download) { Log.Information("Download requested, Installation Path '{Path}', Url '{Url}'", download.InstallPath, download.DownloadUrl); try { Log.Information("Download started..."); + download.Active = true; + download.DownloadState = DownloadState.InProgress; + HttpResponseMessage request = await _httpClient.GetAsync(download.DownloadUrl, HttpCompletionOption.ResponseHeadersRead); await using Stream responseStream = await request.Content.ReadAsStreamAsync(); Log.Information("Download connection established"); @@ -434,70 +431,19 @@ private async Task StartDownloadAsync(Download download) download.Size = request.Content.Headers.ContentLength ?? throw new ContentLengthNullException(download.DownloadUrl); - using IDisposable logProgressDisposable = InstallationService.LogProgress(progressStream, download); + using IDisposable logProgressDisposable = LogProgress(progressStream, download); using IDisposable progressDisposable = progressStream.Progress .Subscribe(p => { download.Downloaded = p; }); - await Task.Run(() => - { - Log.Information("Extracting..."); - try - { - ZipArchive archive = new(progressStream); - - - //TODO UI - Action info = new Action((string log) => - { - Console.WriteLine($"info {log}"); - InfoList.Add(log); - }); - Action errors = new Action((string log) => - { - Console.WriteLine($"error {log}"); - ErrorList.Add(log); - }); - Task scanTask = _codeScanService.OnScan(archive, download.InstallPath, download.GoodFileVersion, - info, errors); - scanTask.Wait(); - if (scanTask.Result) - { - Log.Information("Download completed"); - - _installations.Add(new() - { - BuildVersion = download.BuildVersion, - ForkName = download.ForkName, - InstallationId = Guid.NewGuid(), - InstallationPath = download.InstallPath, - LastPlayedDate = DateTime.Now - }); - - WriteInstallations(); - EnsureExecutableFlagOnUnixSystems(download.InstallPath); - } - else - { - string jsonString = System.Text.Json.JsonSerializer.Serialize(ErrorList); - - string filePath = Path.Combine(_preferencesService.GetPreferences().InstallationPath, "CodeScanErrors.json"); - - File.WriteAllText(filePath, jsonString); - - //TODO UI - Log.Information($"Scan Failed saved to filePath {filePath}"); - } - } - catch (Exception e) - { - Log.Information($"Extracting stopped with {e.ToString()}"); - } - }); + // ExtractAndScan() must be run in a separate thread, but we want this one to wait for that one to finish + // Without this download progress will not work properly + await Task.Run(() => ExtractAndScan(download, progressStream)); } catch (Exception e) { Log.Error(e, "Failed to download Url '{Url}'", download.DownloadUrl); + download.DownloadState = DownloadState.Failed; } finally { @@ -506,6 +452,69 @@ await Task.Run(() => } } + private async Task ExtractAndScan(Download download, ProgressStream progressStream) + { + // TODO: Display infoList and errorList in the UI. + List infoList = new(); + List errorList = new(); + Log.Information("Extracting..."); + try + { + ZipArchive archive = new(progressStream); + + //TODO UI + void Info(string log) + { + Log.Information(log); + infoList.Add(log); + } + + void Errors(string log) + { + Log.Error(log); + errorList.Add(log); + } + + download.DownloadState = DownloadState.Scanning; + bool scanTask = await _codeScanService.OnScanAsync(archive, download.InstallPath, download.GoodFileVersion, + Info, Errors); + + if (scanTask) + { + Log.Information("Download completed"); + + _installations.Add(new() + { + BuildVersion = download.BuildVersion, + ForkName = download.ForkName, + InstallationId = Guid.NewGuid(), + InstallationPath = download.InstallPath, + LastPlayedDate = DateTime.Now + }); + + WriteInstallations(); + EnsureExecutableFlagOnUnixSystems(download.InstallPath); + download.DownloadState = DownloadState.Installed; + } + else + { + string jsonString = JsonSerializer.Serialize(errorList); + string filePath = Path.Combine(_preferencesService.GetPreferences().InstallationPath, "CodeScanErrors.json"); + + await File.WriteAllTextAsync(filePath, jsonString); + + //TODO UI + Log.Error($"Scan failed, saved log to file: {filePath}"); + download.DownloadState = DownloadState.Failed; + } + } + catch (Exception e) + { + Log.Information($"Extracting stopped with {e}"); + download.DownloadState = DownloadState.Failed; + } + } + private static IDisposable LogProgress(ProgressStream progressStream, Download download) { long lastPosition = 0L; diff --git a/UnitystationLauncher/Services/Interface/IAssemblyTypeCheckerService.cs b/UnitystationLauncher/Services/Interface/IAssemblyTypeCheckerService.cs index 51536a44..e4672d95 100644 --- a/UnitystationLauncher/Services/Interface/IAssemblyTypeCheckerService.cs +++ b/UnitystationLauncher/Services/Interface/IAssemblyTypeCheckerService.cs @@ -1,10 +1,11 @@ using System; using System.Collections.Generic; using System.IO; +using System.Threading.Tasks; namespace UnitystationLauncher.Services.Interface; public interface IAssemblyTypeCheckerService { - public bool CheckAssembly(FileInfo diskPath, DirectoryInfo managedPath, List otherAssemblies, Action info, Action errors); + public Task CheckAssemblyTypesAsync(FileInfo diskPath, DirectoryInfo managedPath, List otherAssemblies, Action infoAction, Action errorsAction); } \ No newline at end of file diff --git a/UnitystationLauncher/Services/Interface/ICodeScanConfigService.cs b/UnitystationLauncher/Services/Interface/ICodeScanConfigService.cs index 7e4f24be..a82954e7 100644 --- a/UnitystationLauncher/Services/Interface/ICodeScanConfigService.cs +++ b/UnitystationLauncher/Services/Interface/ICodeScanConfigService.cs @@ -5,11 +5,9 @@ namespace UnitystationLauncher.Services.Interface; public interface ICodeScanConfigService { - public Task<(string, bool)> GetGoodFileVersion(string version); + public Task<(string, bool)> GetGoodFileVersionAsync(string version); - public Task ValidGoodFilesVersion(string goodFileVersion); - - public string SanitiseStringPath(string inString); + public Task ValidGoodFilesVersionAsync(string goodFileVersion); public Task LoadConfigAsync(); } \ No newline at end of file diff --git a/UnitystationLauncher/Services/Interface/ICodeScanService.cs b/UnitystationLauncher/Services/Interface/ICodeScanService.cs index 9ad44d80..5cf109a5 100644 --- a/UnitystationLauncher/Services/Interface/ICodeScanService.cs +++ b/UnitystationLauncher/Services/Interface/ICodeScanService.cs @@ -6,5 +6,5 @@ namespace UnitystationLauncher.Services.Interface; public interface ICodeScanService { - public Task OnScan(ZipArchive archive, string targetDirectory, string goodFileVersion, Action info, Action errors); + public Task OnScanAsync(ZipArchive archive, string targetDirectory, string goodFileVersion, Action info, Action errors); } \ No newline at end of file diff --git a/UnitystationLauncher/Services/Interface/IInstallationService.cs b/UnitystationLauncher/Services/Interface/IInstallationService.cs index d5d53c7d..6f927d67 100644 --- a/UnitystationLauncher/Services/Interface/IInstallationService.cs +++ b/UnitystationLauncher/Services/Interface/IInstallationService.cs @@ -38,7 +38,7 @@ public interface IInstallationService /// /// The server to get the download from /// Download will be `null` if it was unsuccessful in starting, and the string will have the reason - public Task<(Download?, string)> DownloadInstallation(Server server); + public Task<(Download?, string)> DownloadInstallationAsync(Server server); /// /// Starts an installation so we can finally just play the game. diff --git a/UnitystationLauncher/Services/Interface/IPingService.cs b/UnitystationLauncher/Services/Interface/IPingService.cs index 9ca3480b..4af890e4 100644 --- a/UnitystationLauncher/Services/Interface/IPingService.cs +++ b/UnitystationLauncher/Services/Interface/IPingService.cs @@ -5,5 +5,5 @@ namespace UnitystationLauncher.Services.Interface; public interface IPingService { - public Task GetPing(Server server); + public Task GetPingAsync(Server server); } \ No newline at end of file diff --git a/UnitystationLauncher/Services/Interface/IServerService.cs b/UnitystationLauncher/Services/Interface/IServerService.cs index 3dffde84..6fe87f00 100644 --- a/UnitystationLauncher/Services/Interface/IServerService.cs +++ b/UnitystationLauncher/Services/Interface/IServerService.cs @@ -1,4 +1,3 @@ -using System; using System.Collections.Generic; using System.Threading.Tasks; using UnitystationLauncher.Models; diff --git a/UnitystationLauncher/Services/PingService.cs b/UnitystationLauncher/Services/PingService.cs index 91649272..461acb49 100644 --- a/UnitystationLauncher/Services/PingService.cs +++ b/UnitystationLauncher/Services/PingService.cs @@ -20,7 +20,7 @@ public PingService(IEnvironmentService environmentService) _environmentService = environmentService; } - public async Task GetPing(Server server) + public async Task GetPingAsync(Server server) { if (server is { HasValidDomainName: false, HasValidIpAddress: false }) { diff --git a/UnitystationLauncher/Services/ServerService.cs b/UnitystationLauncher/Services/ServerService.cs index 7a14191a..371d4d8e 100644 --- a/UnitystationLauncher/Services/ServerService.cs +++ b/UnitystationLauncher/Services/ServerService.cs @@ -1,9 +1,6 @@ -using System; using System.Collections.Generic; -using System.Globalization; using System.Linq; using System.Net.Http; -using System.Reactive.Linq; using System.Text.Json; using System.Threading.Tasks; using ReactiveUI; diff --git a/UnitystationLauncher/UnitystationLauncher.csproj b/UnitystationLauncher/UnitystationLauncher.csproj index 8d142c2a..f993f2c2 100644 --- a/UnitystationLauncher/UnitystationLauncher.csproj +++ b/UnitystationLauncher/UnitystationLauncher.csproj @@ -34,6 +34,14 @@ NSApplication true + + 4 + true + + + 4 + true + diff --git a/UnitystationLauncher/ViewLocator.cs b/UnitystationLauncher/ViewLocator.cs index 810f2a5c..1f0825b1 100644 --- a/UnitystationLauncher/ViewLocator.cs +++ b/UnitystationLauncher/ViewLocator.cs @@ -14,8 +14,8 @@ public class ViewLocator : IDataTemplate public IControl Build(object data) { - var viewName = data.GetType().FullName!.Replace("ViewModel", "View"); - var type = Type.GetType(viewName); + string viewName = data.GetType().FullName!.Replace("ViewModel", "View"); + Type? type = Type.GetType(viewName); if (type != null) { diff --git a/UnitystationLauncher/ViewModels/InstallationViewModel.cs b/UnitystationLauncher/ViewModels/InstallationViewModel.cs index d96b20b2..3363488a 100644 --- a/UnitystationLauncher/ViewModels/InstallationViewModel.cs +++ b/UnitystationLauncher/ViewModels/InstallationViewModel.cs @@ -1,5 +1,4 @@ using System.Reactive; -using System.Runtime.CompilerServices; using ReactiveUI; using UnitystationLauncher.Models; using UnitystationLauncher.Services.Interface; diff --git a/UnitystationLauncher/ViewModels/LauncherViewModel.cs b/UnitystationLauncher/ViewModels/LauncherViewModel.cs index bb0d9b52..ea3d3e2d 100644 --- a/UnitystationLauncher/ViewModels/LauncherViewModel.cs +++ b/UnitystationLauncher/ViewModels/LauncherViewModel.cs @@ -61,7 +61,7 @@ public LauncherViewModel( OpenPatreon = ReactiveCommand.Create(() => OpenLink(LinkUrls.PatreonUrl)); OpenDiscordInvite = ReactiveCommand.Create(() => OpenLink(LinkUrls.DiscordInviteUrl)); - _panels = LauncherViewModel.GetEnabledPanels(newsPanel, serversPanel, installationsPanel, preferencesPanel); + _panels = GetEnabledPanels(newsPanel, serversPanel, installationsPanel, preferencesPanel); ShowUpdateView = ReactiveCommand.Create(ShowUpdateImp); SelectedPanel = serversPanel; diff --git a/UnitystationLauncher/ViewModels/MainWindowViewModel.cs b/UnitystationLauncher/ViewModels/MainWindowViewModel.cs index 7ed1ff14..8985603c 100644 --- a/UnitystationLauncher/ViewModels/MainWindowViewModel.cs +++ b/UnitystationLauncher/ViewModels/MainWindowViewModel.cs @@ -1,13 +1,8 @@ using System; using ReactiveUI; using System.Reactive.Linq; -using Serilog; using System.Threading; -using System.Reactive; -using System.Reactive.Concurrency; -using System.Threading.Tasks; using Avalonia.Media; -using UnitystationLauncher.Services; namespace UnitystationLauncher.ViewModels { diff --git a/UnitystationLauncher/ViewModels/ServerViewModel.cs b/UnitystationLauncher/ViewModels/ServerViewModel.cs index 363cd473..d553cdee 100644 --- a/UnitystationLauncher/ViewModels/ServerViewModel.cs +++ b/UnitystationLauncher/ViewModels/ServerViewModel.cs @@ -1,22 +1,14 @@ using System; -using System.Net.NetworkInformation; -using System.Reactive.Linq; -using System.Diagnostics; -using System.IO; -using System.Reactive; using System.Reactive.Concurrency; -using System.Text.RegularExpressions; using System.Threading; using System.Threading.Tasks; using Humanizer; -using Reactive.Bindings; using ReactiveUI; using Serilog; using UnitystationLauncher.Models; using UnitystationLauncher.Models.Api; using UnitystationLauncher.Models.Enums; using UnitystationLauncher.Services.Interface; -using ReactiveCommand = ReactiveUI.ReactiveCommand; namespace UnitystationLauncher.ViewModels; @@ -34,11 +26,11 @@ public class ServerViewModel : ViewModelBase public Installation? Installation => _installationService.GetInstallation(Server.ForkName, Server.BuildVersion); - public bool ShowDownloadButton => Installation == null && !ShowDownloadProgress; - - public bool ShowDownloadProgress => Download?.Active ?? false; - + public bool ShowDownloadButton => Installation == null && (Download == null || Download.DownloadState == DownloadState.NotDownloaded); + public bool ShowDownloadProgress => Download?.DownloadState == DownloadState.InProgress; + public bool ShowScanningProgress => Download?.DownloadState == DownloadState.Scanning; public bool ShowStartButton => Installation != null; + public bool ShowDownloadFailed => Download?.DownloadState == DownloadState.Failed; private readonly IInstallationService _installationService; private readonly IPingService _pingService; @@ -67,7 +59,7 @@ public void LaunchGame() _installationService.StartInstallation(Installation.InstallationId, Server.ServerIp, (short)Server.ServerPort); } - private async Task GetPing(IScheduler scheduler, CancellationToken cancellationToken) + private async Task GetPing(IScheduler _, CancellationToken cancellationToken) { if (cancellationToken.IsCancellationRequested) { @@ -76,7 +68,7 @@ private async Task GetPing(IScheduler scheduler, CancellationToken cancellationT try { - Ping = await _pingService.GetPing(Server); + Ping = await _pingService.GetPingAsync(Server); } catch (Exception e) { @@ -107,9 +99,11 @@ private void RefreshDownloadingStatus() this.RaisePropertyChanged(nameof(ShowDownloadProgress)); this.RaisePropertyChanged(nameof(DownloadSize)); this.RaisePropertyChanged(nameof(DownloadedAmount)); + this.RaisePropertyChanged(nameof(ShowScanningProgress)); + this.RaisePropertyChanged(nameof(ShowDownloadFailed)); // Refresh the UI for this server more often while it is downloading. - if (ShowDownloadProgress) + if (ShowDownloadProgress || ShowScanningProgress) { RxApp.MainThreadScheduler.Schedule(DateTimeOffset.Now.AddMilliseconds(200), RefreshDownloadingStatus); } @@ -118,6 +112,12 @@ private void RefreshDownloadingStatus() this.RaisePropertyChanged(nameof(Installation)); this.RaisePropertyChanged(nameof(ShowStartButton)); } + + // Clear out the old Download object once we have an Installation. We won't need it anymore. + if (Download != null && Installation != null) + { + Download = null; + } } } diff --git a/UnitystationLauncher/ViewModels/ServersPanelViewModel.cs b/UnitystationLauncher/ViewModels/ServersPanelViewModel.cs index 454dad0f..55df8874 100644 --- a/UnitystationLauncher/ViewModels/ServersPanelViewModel.cs +++ b/UnitystationLauncher/ViewModels/ServersPanelViewModel.cs @@ -117,7 +117,7 @@ private void RemoveDeletedServers(List servers) private async Task DownloadServer(Server server) { - (Download? download, string downloadFailReason) = await _installationService.DownloadInstallation(server); + (Download? download, string downloadFailReason) = await _installationService.DownloadInstallationAsync(server); if (download == null) { diff --git a/UnitystationLauncher/Views/BlogPostView.axaml b/UnitystationLauncher/Views/BlogPostView.axaml index 25d86276..bbf4e7bc 100644 --- a/UnitystationLauncher/Views/BlogPostView.axaml +++ b/UnitystationLauncher/Views/BlogPostView.axaml @@ -3,8 +3,14 @@ xmlns:d="http://schemas.microsoft.com/expression/blend/2008" xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006" xmlns:asyncImageLoader="clr-namespace:AsyncImageLoader;assembly=AsyncImageLoader.Avalonia" + xmlns:viewModels="clr-namespace:UnitystationLauncher.ViewModels" mc:Ignorable="d" d:DesignWidth="800" d:DesignHeight="450" - x:Class="UnitystationLauncher.Views.BlogPostView"> + x:Class="UnitystationLauncher.Views.BlogPostView" + x:DataType="viewModels:BlogPostViewModel"> + + + + - - - - - - - - - - - + Background="#19212c" + x:DataType="viewModels:MainWindowViewModel"> + + + + + + + + + + + + + + + + + + + + STATION HUB + + + + + + + + - + + + + + + + + \ No newline at end of file diff --git a/UnitystationLauncher/Views/MainWindow.xaml.cs b/UnitystationLauncher/Views/MainWindow.xaml.cs index 6734ebb9..dbe73489 100644 --- a/UnitystationLauncher/Views/MainWindow.xaml.cs +++ b/UnitystationLauncher/Views/MainWindow.xaml.cs @@ -51,7 +51,7 @@ private void SetupTitleBar() _titleBar.IsVisible = false; Grid.SetRow(_contentControl, 0); Grid.SetRowSpan(_contentControl, 2); - _contentControl.BorderThickness = new Thickness(); + _contentControl.BorderThickness = new(); } else { @@ -87,12 +87,12 @@ private void ToggleMaximizeWindowState(object? sender, RoutedEventArgs eventArgs if (WindowState == WindowState.Maximized) { WindowState = WindowState.Normal; - _contentControl.BorderThickness = new Thickness(0.4, 0, 0.4, 0.4); + _contentControl.BorderThickness = new(0.4, 0, 0.4, 0.4); } else if (WindowState == WindowState.Normal) { WindowState = WindowState.Maximized; - _contentControl.BorderThickness = new Thickness(); + _contentControl.BorderThickness = new(); } } diff --git a/UnitystationLauncher/Views/NewsPanelView.xaml b/UnitystationLauncher/Views/NewsPanelView.xaml index a9507bfb..506abe38 100644 --- a/UnitystationLauncher/Views/NewsPanelView.xaml +++ b/UnitystationLauncher/Views/NewsPanelView.xaml @@ -2,8 +2,13 @@ xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml" xmlns:d="http://schemas.microsoft.com/expression/blend/2008" xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006" + xmlns:viewModels="clr-namespace:UnitystationLauncher.ViewModels" mc:Ignorable="d" d:DesignWidth="800" d:DesignHeight="450" - x:Class="UnitystationLauncher.Views.NewsPanelView"> + x:Class="UnitystationLauncher.Views.NewsPanelView" + x:DataType="viewModels:NewsPanelViewModel"> + + + diff --git a/UnitystationLauncher/Views/PopUpDialogue.cs b/UnitystationLauncher/Views/PopUpDialogue.cs index 9dd0f245..267c3aae 100644 --- a/UnitystationLauncher/Views/PopUpDialogue.cs +++ b/UnitystationLauncher/Views/PopUpDialogue.cs @@ -1,10 +1,7 @@ using System; -using System.Windows.Input; -using Avalonia; using Avalonia.Controls; using Avalonia.Interactivity; using Avalonia.Markup.Xaml; -using MessageBox.Avalonia.ViewModels.Commands; namespace UnitystationLauncher.Views { diff --git a/UnitystationLauncher/Views/PopUpDialogue.xaml b/UnitystationLauncher/Views/PopUpDialogue.xaml index 001790ff..921bfec4 100644 --- a/UnitystationLauncher/Views/PopUpDialogue.xaml +++ b/UnitystationLauncher/Views/PopUpDialogue.xaml @@ -2,6 +2,7 @@ xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml" xmlns:d="http://schemas.microsoft.com/expression/blend/2008" xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006" + xmlns:viewModels="clr-namespace:UnitystationLauncher.ViewModels" Width="300" Height="150" mc:Ignorable="d" d:DesignWidth="800" d:DesignHeight="450" x:Class="UnitystationLauncher.Views.PopUpDialogue"> diff --git a/UnitystationLauncher/Views/PreferencesPanelView.xaml b/UnitystationLauncher/Views/PreferencesPanelView.xaml index 425d1356..24268253 100644 --- a/UnitystationLauncher/Views/PreferencesPanelView.xaml +++ b/UnitystationLauncher/Views/PreferencesPanelView.xaml @@ -2,8 +2,14 @@ xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml" xmlns:d="http://schemas.microsoft.com/expression/blend/2008" xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006" + xmlns:viewModels="clr-namespace:UnitystationLauncher.ViewModels" mc:Ignorable="d" d:DesignWidth="800" d:DesignHeight="450" - x:Class="UnitystationLauncher.Views.PreferencesPanelView"> + x:Class="UnitystationLauncher.Views.PreferencesPanelView" + x:DataType="viewModels:PreferencesPanelViewModel"> + + + + diff --git a/UnitystationLauncher/Views/PreferencesPanelView.xaml.cs b/UnitystationLauncher/Views/PreferencesPanelView.xaml.cs index 8ad6bddf..dee0f8e3 100644 --- a/UnitystationLauncher/Views/PreferencesPanelView.xaml.cs +++ b/UnitystationLauncher/Views/PreferencesPanelView.xaml.cs @@ -1,16 +1,12 @@ -using System; -using System.Diagnostics; using System.Reactive.Concurrency; using System.Threading.Tasks; using Avalonia; using Avalonia.Controls; using Avalonia.Controls.ApplicationLifetimes; -using Avalonia.Data; using Avalonia.Interactivity; using Avalonia.Markup.Xaml; using ReactiveUI; using Serilog; -using UnitystationLauncher.Models.Enums; using UnitystationLauncher.ViewModels; namespace UnitystationLauncher.Views diff --git a/UnitystationLauncher/Views/ServerView.axaml b/UnitystationLauncher/Views/ServerView.axaml index c0f0ea6a..0f3452f2 100644 --- a/UnitystationLauncher/Views/ServerView.axaml +++ b/UnitystationLauncher/Views/ServerView.axaml @@ -2,8 +2,14 @@ xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml" xmlns:d="http://schemas.microsoft.com/expression/blend/2008" xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006" + xmlns:viewModels="clr-namespace:UnitystationLauncher.ViewModels" mc:Ignorable="d" d:DesignWidth="800" d:DesignHeight="100" - x:Class="UnitystationLauncher.Views.ServerView" FontSize="17"> + x:Class="UnitystationLauncher.Views.ServerView" FontSize="17" + x:DataType="viewModels:ServerViewModel"> + + + +