@ -0,0 +1,246 @@ | |||
# Remove the line below if you want to inherit .editorconfig settings from higher directories | |||
root = true | |||
# C# files | |||
[*.cs] | |||
#### Core EditorConfig Options #### | |||
# Indentation and spacing | |||
indent_size = 4 | |||
indent_style = space | |||
tab_width = 4 | |||
# New line preferences | |||
end_of_line = crlf | |||
insert_final_newline = false | |||
#### .NET Coding Conventions #### | |||
# Organize usings | |||
dotnet_separate_import_directive_groups = false | |||
dotnet_sort_system_directives_first = false | |||
file_header_template = unset | |||
# this. and Me. preferences | |||
dotnet_style_qualification_for_event = false | |||
dotnet_style_qualification_for_field = false | |||
dotnet_style_qualification_for_method = false | |||
dotnet_style_qualification_for_property = false | |||
# Language keywords vs BCL types preferences | |||
dotnet_style_predefined_type_for_locals_parameters_members = true | |||
dotnet_style_predefined_type_for_member_access = true | |||
# Parentheses preferences | |||
dotnet_style_parentheses_in_arithmetic_binary_operators = always_for_clarity:warning | |||
dotnet_style_parentheses_in_other_binary_operators = always_for_clarity:warning | |||
dotnet_style_parentheses_in_other_operators = never_if_unnecessary:suggestion | |||
dotnet_style_parentheses_in_relational_binary_operators = always_for_clarity:warning | |||
# Modifier preferences | |||
dotnet_style_require_accessibility_modifiers = for_non_interface_members | |||
# Expression-level preferences | |||
dotnet_style_coalesce_expression = true | |||
dotnet_style_collection_initializer = true | |||
dotnet_style_explicit_tuple_names = true | |||
dotnet_style_namespace_match_folder = true | |||
dotnet_style_null_propagation = true | |||
dotnet_style_object_initializer = true | |||
dotnet_style_operator_placement_when_wrapping = beginning_of_line | |||
dotnet_style_prefer_auto_properties = true:warning | |||
dotnet_style_prefer_compound_assignment = true:warning | |||
dotnet_style_prefer_conditional_expression_over_assignment = true:warning | |||
dotnet_style_prefer_conditional_expression_over_return = true:warning | |||
dotnet_style_prefer_foreach_explicit_cast_in_source = when_strongly_typed | |||
dotnet_style_prefer_inferred_anonymous_type_member_names = true | |||
dotnet_style_prefer_inferred_tuple_names = true | |||
dotnet_style_prefer_is_null_check_over_reference_equality_method = true:warning | |||
dotnet_style_prefer_simplified_boolean_expressions = true:warning | |||
dotnet_style_prefer_simplified_interpolation = true | |||
# Field preferences | |||
dotnet_style_readonly_field = true | |||
# Parameter preferences | |||
dotnet_code_quality_unused_parameters = all | |||
# Suppression preferences | |||
dotnet_remove_unnecessary_suppression_exclusions = none | |||
# New line preferences | |||
dotnet_style_allow_multiple_blank_lines_experimental = true | |||
dotnet_style_allow_statement_immediately_after_block_experimental = true | |||
#### C# Coding Conventions #### | |||
# var preferences | |||
csharp_style_var_elsewhere = false | |||
csharp_style_var_for_built_in_types = false | |||
csharp_style_var_when_type_is_apparent = false | |||
# Expression-bodied members | |||
csharp_style_expression_bodied_accessors = true:silent | |||
csharp_style_expression_bodied_constructors = false:silent | |||
csharp_style_expression_bodied_indexers = true:silent | |||
csharp_style_expression_bodied_lambdas = true:suggestion | |||
csharp_style_expression_bodied_local_functions = when_on_single_line:suggestion | |||
csharp_style_expression_bodied_methods = false:silent | |||
csharp_style_expression_bodied_operators = false:silent | |||
csharp_style_expression_bodied_properties = true:silent | |||
# Pattern matching preferences | |||
csharp_style_pattern_matching_over_as_with_null_check = true | |||
csharp_style_pattern_matching_over_is_with_cast_check = true | |||
csharp_style_prefer_extended_property_pattern = true | |||
csharp_style_prefer_not_pattern = true:warning | |||
csharp_style_prefer_pattern_matching = true:warning | |||
csharp_style_prefer_switch_expression = true | |||
# Null-checking preferences | |||
csharp_style_conditional_delegate_call = true | |||
# Modifier preferences | |||
csharp_prefer_static_local_function = true:warning | |||
csharp_preferred_modifier_order = public,private,protected,internal,static,extern,new,virtual,abstract,sealed,override,readonly,unsafe,required,volatile,async | |||
# Code-block preferences | |||
csharp_prefer_braces = true:silent | |||
csharp_prefer_simple_using_statement = true:suggestion | |||
csharp_style_namespace_declarations = block_scoped:silent | |||
csharp_style_prefer_method_group_conversion = true:silent | |||
csharp_style_prefer_top_level_statements = true:silent | |||
# Expression-level preferences | |||
csharp_prefer_simple_default_expression = true | |||
csharp_style_deconstructed_variable_declaration = true | |||
csharp_style_implicit_object_creation_when_type_is_apparent = true:warning | |||
csharp_style_inlined_variable_declaration = true | |||
csharp_style_prefer_index_operator = true:warning | |||
csharp_style_prefer_local_over_anonymous_function = true:warning | |||
csharp_style_prefer_null_check_over_type_check = true | |||
csharp_style_prefer_range_operator = true | |||
csharp_style_prefer_tuple_swap = true | |||
csharp_style_prefer_utf8_string_literals = true | |||
csharp_style_throw_expression = true | |||
csharp_style_unused_value_assignment_preference = discard_variable:warning | |||
csharp_style_unused_value_expression_statement_preference = discard_variable:warning | |||
# 'using' directive preferences | |||
csharp_using_directive_placement = outside_namespace:warning | |||
# New line preferences | |||
csharp_style_allow_blank_line_after_colon_in_constructor_initializer_experimental = true | |||
csharp_style_allow_blank_lines_between_consecutive_braces_experimental = true | |||
csharp_style_allow_embedded_statements_on_same_line_experimental = true | |||
#### C# Formatting Rules #### | |||
# New line preferences | |||
csharp_new_line_before_catch = true | |||
csharp_new_line_before_else = true | |||
csharp_new_line_before_finally = true | |||
csharp_new_line_before_members_in_anonymous_types = true | |||
csharp_new_line_before_members_in_object_initializers = true | |||
csharp_new_line_before_open_brace = all | |||
csharp_new_line_between_query_expression_clauses = true | |||
# Indentation preferences | |||
csharp_indent_block_contents = true | |||
csharp_indent_braces = false | |||
csharp_indent_case_contents = true | |||
csharp_indent_case_contents_when_block = true | |||
csharp_indent_labels = one_less_than_current | |||
csharp_indent_switch_labels = true | |||
# Space preferences | |||
csharp_space_after_cast = false | |||
csharp_space_after_colon_in_inheritance_clause = true | |||
csharp_space_after_comma = true | |||
csharp_space_after_dot = false | |||
csharp_space_after_keywords_in_control_flow_statements = true | |||
csharp_space_after_semicolon_in_for_statement = true | |||
csharp_space_around_binary_operators = before_and_after | |||
csharp_space_around_declaration_statements = false | |||
csharp_space_before_colon_in_inheritance_clause = true | |||
csharp_space_before_comma = false | |||
csharp_space_before_dot = false | |||
csharp_space_before_open_square_brackets = false | |||
csharp_space_before_semicolon_in_for_statement = false | |||
csharp_space_between_empty_square_brackets = false | |||
csharp_space_between_method_call_empty_parameter_list_parentheses = false | |||
csharp_space_between_method_call_name_and_opening_parenthesis = false | |||
csharp_space_between_method_call_parameter_list_parentheses = false | |||
csharp_space_between_method_declaration_empty_parameter_list_parentheses = false | |||
csharp_space_between_method_declaration_name_and_open_parenthesis = false | |||
csharp_space_between_method_declaration_parameter_list_parentheses = false | |||
csharp_space_between_parentheses = false | |||
csharp_space_between_square_brackets = false | |||
# Wrapping preferences | |||
csharp_preserve_single_line_blocks = true | |||
csharp_preserve_single_line_statements = true | |||
#### Naming styles #### | |||
# Naming rules | |||
dotnet_naming_rule.interface_should_be_begins_with_i.severity = suggestion | |||
dotnet_naming_rule.interface_should_be_begins_with_i.symbols = interface | |||
dotnet_naming_rule.interface_should_be_begins_with_i.style = begins_with_i | |||
dotnet_naming_rule.types_should_be_pascal_case.severity = suggestion | |||
dotnet_naming_rule.types_should_be_pascal_case.symbols = types | |||
dotnet_naming_rule.types_should_be_pascal_case.style = pascal_case | |||
dotnet_naming_rule.non_field_members_should_be_pascal_case.severity = suggestion | |||
dotnet_naming_rule.non_field_members_should_be_pascal_case.symbols = non_field_members | |||
dotnet_naming_rule.non_field_members_should_be_pascal_case.style = pascal_case | |||
# Symbol specifications | |||
dotnet_naming_symbols.interface.applicable_kinds = interface | |||
dotnet_naming_symbols.interface.applicable_accessibilities = public, internal, private, protected, protected_internal, private_protected | |||
dotnet_naming_symbols.interface.required_modifiers = | |||
dotnet_naming_symbols.types.applicable_kinds = class, struct, interface, enum | |||
dotnet_naming_symbols.types.applicable_accessibilities = public, internal, private, protected, protected_internal, private_protected | |||
dotnet_naming_symbols.types.required_modifiers = | |||
dotnet_naming_symbols.non_field_members.applicable_kinds = property, event, method | |||
dotnet_naming_symbols.non_field_members.applicable_accessibilities = public, internal, private, protected, protected_internal, private_protected | |||
dotnet_naming_symbols.non_field_members.required_modifiers = | |||
# Naming styles | |||
dotnet_naming_style.pascal_case.required_prefix = | |||
dotnet_naming_style.pascal_case.required_suffix = | |||
dotnet_naming_style.pascal_case.word_separator = | |||
dotnet_naming_style.pascal_case.capitalization = pascal_case | |||
dotnet_naming_style.begins_with_i.required_prefix = I | |||
dotnet_naming_style.begins_with_i.required_suffix = | |||
dotnet_naming_style.begins_with_i.word_separator = | |||
dotnet_naming_style.begins_with_i.capitalization = pascal_case | |||
[*.{cs,vb}] | |||
dotnet_style_operator_placement_when_wrapping = beginning_of_line | |||
tab_width = 4 | |||
indent_size = 4 | |||
end_of_line = crlf | |||
dotnet_style_coalesce_expression = true:suggestion | |||
dotnet_style_null_propagation = true:suggestion | |||
dotnet_style_prefer_is_null_check_over_reference_equality_method = true:warning | |||
dotnet_style_prefer_auto_properties = true:warning | |||
dotnet_style_object_initializer = true:suggestion | |||
dotnet_style_collection_initializer = true:suggestion | |||
dotnet_style_prefer_simplified_boolean_expressions = true:warning | |||
dotnet_style_prefer_conditional_expression_over_assignment = true:warning | |||
dotnet_style_prefer_conditional_expression_over_return = true:warning | |||
dotnet_style_explicit_tuple_names = true:suggestion | |||
dotnet_style_prefer_inferred_tuple_names = true:suggestion | |||
dotnet_style_prefer_inferred_anonymous_type_member_names = true:suggestion | |||
dotnet_style_prefer_compound_assignment = true:warning | |||
dotnet_style_prefer_simplified_interpolation = true:suggestion | |||
dotnet_style_namespace_match_folder = true:suggestion |
@ -0,0 +1,12 @@ | |||
{ | |||
"version": "0.2.0", | |||
"configurations": [ | |||
{ | |||
"name": "Attach to BSIPA", | |||
"type": "mono", | |||
"request": "attach", | |||
"address": "localhost", | |||
"port": 10000 | |||
} | |||
] | |||
} |
@ -1 +1 @@ | |||
Subproject commit 41c3a12d56de96a3495893d1fea4a485a98c67af | |||
Subproject commit b04769a3aebdd111b81f5a59d438907310e83207 |
@ -1,8 +1,4 @@ | |||
<?xml version="1.0" encoding="utf-8"?> | |||
<Project> | |||
<ItemGroup> | |||
<Compile Include="$(MSBuildThisFileDirectory)\System.Diagnostics.CodeAnalysis.cs"/> | |||
</ItemGroup> | |||
</Project> |
@ -1,73 +1,73 @@ | |||
using IPA.Logging; | |||
using System; | |||
using System; | |||
using System.Diagnostics; | |||
using System.Diagnostics.CodeAnalysis; | |||
using System.IO; | |||
using System.Security.AccessControl; | |||
using System.Security.Principal; | |||
using System.Threading.Tasks; | |||
#if NET3 | |||
using Net3_Proxy; | |||
#endif | |||
namespace IPA.Injector | |||
{ | |||
internal static class PermissionFix | |||
using System.IO; | |||
using System.Security.AccessControl; | |||
using System.Security.Principal; | |||
using System.Threading.Tasks; | |||
#if NET3 | |||
using Net3_Proxy; | |||
#endif | |||
namespace IPA.Injector | |||
{ | |||
internal static class PermissionFix | |||
{ | |||
[SuppressMessage("Reliability", "CA2008:Do not create tasks without passing a TaskScheduler", | |||
Justification = "I very explicitly want the default scheduler")] | |||
public static Task FixPermissions(DirectoryInfo root) | |||
{ | |||
if (!root.Exists) return new Task(() => { }); | |||
return Task.Factory.StartNew(() => | |||
{ | |||
var sw = Stopwatch.StartNew(); | |||
try | |||
{ | |||
var acl = root.GetAccessControl(); | |||
var rules = acl.GetAccessRules(true, true, typeof(SecurityIdentifier)); | |||
var requestedRights = FileSystemRights.Modify; | |||
var requestedInheritance = InheritanceFlags.ContainerInherit | InheritanceFlags.ObjectInherit; | |||
var requestedPropagation = PropagationFlags.InheritOnly; | |||
bool hasRule = false; | |||
for (var i = 0; i < rules.Count; i++) | |||
{ | |||
var rule = rules[i]; | |||
if (rule is FileSystemAccessRule fsrule | |||
&& fsrule.AccessControlType == AccessControlType.Allow | |||
&& fsrule.InheritanceFlags.HasFlag(requestedInheritance) | |||
&& fsrule.PropagationFlags == requestedPropagation | |||
&& fsrule.FileSystemRights.HasFlag(requestedRights)) | |||
{ hasRule = true; break; } | |||
} | |||
if (!hasRule) | |||
{ // this is *sooo* fucking slow on first run | |||
acl.AddAccessRule( | |||
new FileSystemAccessRule( | |||
new SecurityIdentifier(WellKnownSidType.WorldSid, null), | |||
requestedRights, | |||
requestedInheritance, | |||
requestedPropagation, | |||
AccessControlType.Allow | |||
) | |||
); | |||
root.SetAccessControl(acl); | |||
} | |||
} | |||
catch (Exception e) | |||
{ | |||
Logger.log.Warn("Error configuring permissions in the game install dir"); | |||
Logger.log.Warn(e); | |||
} | |||
sw.Stop(); | |||
Logger.log.Info($"Configuring permissions took {sw.Elapsed}"); | |||
}); | |||
} | |||
} | |||
} | |||
public static Task FixPermissions(DirectoryInfo root) | |||
{ | |||
if (!root.Exists) return new Task(() => { }); | |||
return Task.Factory.StartNew(() => | |||
{ | |||
var sw = Stopwatch.StartNew(); | |||
try | |||
{ | |||
var acl = root.GetAccessControl(); | |||
var rules = acl.GetAccessRules(true, true, typeof(SecurityIdentifier)); | |||
var requestedRights = FileSystemRights.Modify; | |||
var requestedInheritance = InheritanceFlags.ContainerInherit | InheritanceFlags.ObjectInherit; | |||
var requestedPropagation = PropagationFlags.InheritOnly; | |||
bool hasRule = false; | |||
for (var i = 0; i < rules.Count; i++) | |||
{ | |||
var rule = rules[i]; | |||
if (rule is FileSystemAccessRule fsrule | |||
&& fsrule.AccessControlType == AccessControlType.Allow | |||
&& fsrule.InheritanceFlags.HasFlag(requestedInheritance) | |||
&& fsrule.PropagationFlags == requestedPropagation | |||
&& fsrule.FileSystemRights.HasFlag(requestedRights)) | |||
{ hasRule = true; break; } | |||
} | |||
if (!hasRule) | |||
{ // this is *sooo* fucking slow on first run | |||
acl.AddAccessRule( | |||
new FileSystemAccessRule( | |||
new SecurityIdentifier(WellKnownSidType.WorldSid, null), | |||
requestedRights, | |||
requestedInheritance, | |||
requestedPropagation, | |||
AccessControlType.Allow | |||
) | |||
); | |||
root.SetAccessControl(acl); | |||
} | |||
} | |||
catch (Exception e) | |||
{ | |||
Logger.Default.Warn("Error configuring permissions in the game install dir"); | |||
Logger.Default.Warn(e); | |||
} | |||
sw.Stop(); | |||
Logger.Default.Info($"Configuring permissions took {sw.Elapsed}"); | |||
}); | |||
} | |||
} | |||
} |
@ -1,37 +1,39 @@ | |||
using System.Reflection; | |||
using System.Runtime.CompilerServices; | |||
using System.Runtime.InteropServices; | |||
// General Information about an assembly is controlled through the following | |||
// set of attributes. Change these attribute values to modify the information | |||
// associated with an assembly. | |||
[assembly: AssemblyTitle("IPA.Injector")] | |||
[assembly: AssemblyDescription("")] | |||
[assembly: AssemblyConfiguration("")] | |||
[assembly: AssemblyCompany("")] | |||
[assembly: AssemblyProduct("IPA.Injector")] | |||
[assembly: AssemblyCopyright("Copyright © 2018")] | |||
[assembly: AssemblyTrademark("")] | |||
[assembly: AssemblyCulture("")] | |||
// Setting ComVisible to false makes the types in this assembly not visible | |||
// to COM components. If you need to access a type in this assembly from | |||
// COM, set the ComVisible attribute to true on that type. | |||
[assembly: ComVisible(false)] | |||
// The following GUID is for the ID of the typelib if this project is exposed to COM | |||
[assembly: Guid("2a1af16b-27f1-46e0-9a95-181516bc1cb7")] | |||
[assembly: InternalsVisibleTo("IPA.Loader")] | |||
// Version information for an assembly consists of the following four values: | |||
// | |||
// Major Version | |||
// Minor Version | |||
// Build Number | |||
// Revision | |||
// | |||
// You can specify all the values or you can default the Build and Revision Numbers | |||
// by using the '*' as shown below: | |||
// [assembly: AssemblyVersion("1.0.*")] | |||
[assembly: AssemblyVersion(IPA.Config.SelfConfig.IPAVersion)] | |||
using System; | |||
using System.Reflection; | |||
using System.Runtime.CompilerServices; | |||
using System.Runtime.InteropServices; | |||
// General Information about an assembly is controlled through the following | |||
// set of attributes. Change these attribute values to modify the information | |||
// associated with an assembly. | |||
[assembly: AssemblyTitle("IPA.Injector")] | |||
[assembly: AssemblyDescription("")] | |||
[assembly: AssemblyConfiguration("")] | |||
[assembly: AssemblyCompany("")] | |||
[assembly: AssemblyProduct("IPA.Injector")] | |||
[assembly: AssemblyCopyright("Copyright © 2018")] | |||
[assembly: AssemblyTrademark("")] | |||
[assembly: AssemblyCulture("")] | |||
// Setting ComVisible to false makes the types in this assembly not visible | |||
// to COM components. If you need to access a type in this assembly from | |||
// COM, set the ComVisible attribute to true on that type. | |||
[assembly: ComVisible(false)] | |||
[assembly: CLSCompliant(false)] | |||
// The following GUID is for the ID of the typelib if this project is exposed to COM | |||
[assembly: Guid("2a1af16b-27f1-46e0-9a95-181516bc1cb7")] | |||
[assembly: InternalsVisibleTo("IPA.Loader")] | |||
// Version information for an assembly consists of the following four values: | |||
// | |||
// Major Version | |||
// Minor Version | |||
// Build Number | |||
// Revision | |||
// | |||
// You can specify all the values or you can default the Build and Revision Numbers | |||
// by using the '*' as shown below: | |||
// [assembly: AssemblyVersion("1.0.*")] | |||
[assembly: AssemblyVersion(IPA.Config.SelfConfig.IPAVersion)] | |||
[assembly: AssemblyFileVersion(IPA.Config.SelfConfig.IPAVersion)] |
@ -0,0 +1,14 @@ | |||
#nullable enable | |||
using System; | |||
namespace IPA.AntiMalware | |||
{ | |||
internal static class AmsiConstants | |||
{ | |||
public const string AppName = "BSIPA/" + Config.SelfConfig.IPAVersion; | |||
public const string IAntimalwareGuidStr = "82d29c2e-f062-44e6-b5c9-3d9a2f24a2df"; | |||
public static readonly Guid IAntimalwareGuid = new(IAntimalwareGuidStr); | |||
public static readonly Guid CAntimalwareGuid = new("fdb00e52-a214-4aa1-8fba-4357bb0072ec"); | |||
public static readonly Guid IUnknownGuid = new("00000000-0000-0000-C000-000000000046"); | |||
} | |||
} |
@ -0,0 +1,13 @@ | |||
#nullable enable | |||
namespace IPA.AntiMalware | |||
{ | |||
internal enum AmsiResult | |||
{ | |||
Clean = 0, | |||
NotDetected = 1, | |||
BlockedByAdminStart = 0x4000, | |||
BlockedByAdminEnd = 0x4fff, | |||
Detected = 32768 | |||
} | |||
} |
@ -0,0 +1,46 @@ | |||
#nullable enable | |||
using IPA.Config; | |||
using IPA.Logging; | |||
using System; | |||
namespace IPA.AntiMalware | |||
{ | |||
/// <summary> | |||
/// Provides a way to access BSIPA's Anti-Malware engine. | |||
/// </summary> | |||
/// <see cref="Engine"/> | |||
/// <see cref="IAntiMalware"/> | |||
public static class AntiMalwareEngine | |||
{ | |||
private static IAntiMalware? engine; | |||
/// <summary> | |||
/// Gets the current Anti-Malware engine. | |||
/// </summary> | |||
public static IAntiMalware Engine => engine ?? throw new InvalidOperationException(); | |||
internal static bool IsInitialized => engine != null; | |||
internal static void Initialize() | |||
{ | |||
engine = CreateEngine(); | |||
} | |||
private static IAntiMalware CreateEngine() | |||
{ | |||
IAntiMalware? engine = null; | |||
if (SelfConfig.AntiMalware_.UseIfAvailable_) | |||
{ | |||
#if !NET35 | |||
engine = WindowsCOMAntiMalware.TryInitialize(); | |||
#endif | |||
engine ??= WindowsWin32AntiMalware.TryInitialize(); | |||
} | |||
engine ??= new NoopAntiMalware(); | |||
Logger.AntiMalware.Debug($"Antimalware engine initialized with {engine.GetType()}"); | |||
return engine; | |||
} | |||
} | |||
} |
@ -0,0 +1,25 @@ | |||
#nullable enable | |||
using System.IO; | |||
namespace IPA.AntiMalware | |||
{ | |||
/// <summary> | |||
/// An Anti-Malware engine that can be used to scan and detect potentially harmful files. | |||
/// </summary> | |||
public interface IAntiMalware | |||
{ | |||
/// <summary> | |||
/// Scans a particular file for malware. | |||
/// </summary> | |||
/// <param name="file">The file to scan.</param> | |||
/// <returns>A <see cref="ScanResult"/> indicating whether the file is safe or not.</returns> | |||
ScanResult ScanFile(FileInfo file); | |||
/// <summary> | |||
/// Scans a particular in-memory blob for malware. | |||
/// </summary> | |||
/// <param name="data">The binary blob to scan.</param> | |||
/// <param name="contentName">The name of the content. If this is left <see langword="null"/>, one will be automatically generated.</param> | |||
/// <returns>A <see cref="ScanResult"/> indicating whether the file is safe or not.</returns> | |||
ScanResult ScanData(byte[] data, string? contentName = null); | |||
} | |||
} |
@ -0,0 +1,15 @@ | |||
using System; | |||
using System.Collections.Generic; | |||
using System.IO; | |||
using System.Linq; | |||
using System.Text; | |||
using System.Threading.Tasks; | |||
namespace IPA.AntiMalware | |||
{ | |||
internal class NoopAntiMalware : IAntiMalware | |||
{ | |||
public ScanResult ScanData(byte[] data, string contentName = null) => ScanResult.NotDetected; | |||
public ScanResult ScanFile(FileInfo file) => ScanResult.NotDetected; | |||
} | |||
} |
@ -0,0 +1,27 @@ | |||
| |||
namespace IPA.AntiMalware | |||
{ | |||
/// <summary> | |||
/// The result of an Anti-Malware scan. | |||
/// </summary> | |||
public enum ScanResult | |||
{ | |||
/// <summary> | |||
/// The object is known to be safe. | |||
/// </summary> | |||
KnownSafe, | |||
/// <summary> | |||
/// No malware was detected, but it is not known to be safe. | |||
/// </summary> | |||
NotDetected, | |||
/// <summary> | |||
/// Malware was detected, and the content should not be executed. | |||
/// </summary> | |||
Detected, | |||
/// <summary> | |||
/// The malware engine returned a threat level less than the max, so this object may be dangerous. | |||
/// Proceed with caution. | |||
/// </summary> | |||
MaybeMalware | |||
} | |||
} |
@ -0,0 +1,111 @@ | |||
#nullable enable | |||
using IPA.Logging; | |||
using System; | |||
using System.Collections.Generic; | |||
using System.IO; | |||
using System.Linq; | |||
using System.Runtime.InteropServices; | |||
using System.Text; | |||
using System.Threading.Tasks; | |||
namespace IPA.AntiMalware | |||
{ | |||
internal class WindowsWin32AntiMalware : IAntiMalware, IDisposable | |||
{ | |||
internal static WindowsWin32AntiMalware? TryInitialize() | |||
{ | |||
try | |||
{ | |||
return new(); | |||
} | |||
catch (Exception e) | |||
{ | |||
Logger.AntiMalware.Warn("Could not initialize Win32-based antimalware engine:"); | |||
Logger.AntiMalware.Warn(e); | |||
return null; | |||
} | |||
} | |||
private readonly IntPtr handle; | |||
private bool disposedValue; | |||
private WindowsWin32AntiMalware() | |||
{ | |||
AmsiInitialize(AmsiConstants.AppName, out handle); | |||
} | |||
private static ScanResult ScanResultFromAmsiResult(AmsiResult result) | |||
=> result switch | |||
{ | |||
AmsiResult.Clean => ScanResult.KnownSafe, | |||
AmsiResult.NotDetected => ScanResult.NotDetected, | |||
AmsiResult.Detected => ScanResult.Detected, | |||
_ => ScanResult.MaybeMalware | |||
}; | |||
public ScanResult ScanFile(FileInfo file) | |||
{ | |||
var data = File.ReadAllBytes(file.FullName); | |||
return ScanData(data, file.FullName); | |||
} | |||
public ScanResult ScanData(byte[] data, string? contentName = null) | |||
{ | |||
contentName ??= $"unknown_data_{Guid.NewGuid()}"; | |||
AmsiScanBuffer(handle, data, (uint)data.Length, contentName, IntPtr.Zero, out var result); | |||
Logger.AntiMalware.Trace($"Scanned data named '{contentName}' and got '{result}'"); | |||
return ScanResultFromAmsiResult(result); | |||
} | |||
protected virtual void Dispose(bool disposing) | |||
{ | |||
if (!disposedValue) | |||
{ | |||
if (disposing) | |||
{ | |||
// we have no disposable managed state | |||
} | |||
AmsiUninitialize(handle); | |||
disposedValue = true; | |||
} | |||
} | |||
~WindowsWin32AntiMalware() | |||
{ | |||
// Do not change this code. Put cleanup code in 'Dispose(bool disposing)' method | |||
Dispose(disposing: false); | |||
} | |||
public void Dispose() | |||
{ | |||
// Do not change this code. Put cleanup code in 'Dispose(bool disposing)' method | |||
Dispose(disposing: true); | |||
GC.SuppressFinalize(this); | |||
} | |||
[DllImport("amsi", CallingConvention = CallingConvention.Winapi, CharSet = CharSet.Unicode, ExactSpelling = true)] | |||
#if !NET35 | |||
[DefaultDllImportSearchPaths(DllImportSearchPath.System32)] | |||
#endif | |||
private static extern void AmsiInitialize([MarshalAs(UnmanagedType.LPWStr)] string appName, [Out] out IntPtr handle); | |||
[DllImport("amsi", CallingConvention = CallingConvention.Winapi, CharSet = CharSet.Unicode, ExactSpelling = true)] | |||
#if !NET35 | |||
[DefaultDllImportSearchPaths(DllImportSearchPath.System32)] | |||
#endif | |||
private static extern void AmsiUninitialize(IntPtr handle); | |||
[DllImport("amsi", CallingConvention = CallingConvention.Winapi, CharSet = CharSet.Unicode, ExactSpelling = true)] | |||
#if !NET35 | |||
[DefaultDllImportSearchPaths(DllImportSearchPath.System32)] | |||
#endif | |||
private static extern void AmsiScanBuffer(IntPtr context, | |||
[MarshalAs(UnmanagedType.LPArray, SizeParamIndex = 2)] byte[] buffer, uint length, | |||
[MarshalAs(UnmanagedType.LPWStr)] string contentName, | |||
IntPtr session, | |||
[Out] out AmsiResult result); | |||
} | |||
} |
@ -0,0 +1,113 @@ | |||
#nullable enable | |||
using System; | |||
using System.Collections.Generic; | |||
using System.IO; | |||
using System.Linq; | |||
using System.Runtime.InteropServices; | |||
using System.Text; | |||
using System.Threading.Tasks; | |||
namespace IPA.AntiMalware.ComAPI | |||
{ | |||
internal class AmsiFileStream : IAmsiStream, IDisposable | |||
{ | |||
private readonly FileInfo file; | |||
private readonly IntPtr session; | |||
public AmsiFileStream(FileInfo file, IntPtr session) | |||
{ | |||
this.file = file; | |||
this.session = session; | |||
} | |||
public unsafe void GetAttribute([In] AmsiAttribute attribute, [In] uint dataSize, [Out] byte* buffer, out uint writtenData) | |||
{ | |||
switch (attribute) | |||
{ | |||
case AmsiAttribute.AppName: | |||
writtenData = WriteWString(AmsiConstants.AppName, dataSize, buffer); | |||
return; | |||
case AmsiAttribute.Session: | |||
*(IntPtr*)buffer = session; | |||
writtenData = (uint)sizeof(IntPtr); | |||
return; | |||
case AmsiAttribute.ContentName: | |||
writtenData = WriteWString(file.FullName, dataSize, buffer); | |||
return; | |||
case AmsiAttribute.ContentSize: | |||
*(ulong*)buffer = (ulong)file.Length; | |||
writtenData = sizeof(ulong); | |||
return; | |||
default: | |||
throw new NotImplementedException(); // return e_notimpl | |||
} | |||
static unsafe uint WriteWString(string str, uint dataSize, byte* buffer) | |||
{ | |||
fixed (char* name = str) | |||
{ | |||
return (uint)Encoding.Unicode.GetBytes(name, str.Length, buffer, (int)dataSize); | |||
} | |||
} | |||
} | |||
private FileStream? stream; | |||
private bool disposedValue; | |||
private readonly byte[] readBuffer = new byte[1024]; | |||
public unsafe void Read([In] ulong position, [In] uint dataSize, [Out] byte* buffer, out uint readSize) | |||
{ | |||
stream ??= file.OpenRead(); | |||
stream.Position = (long)position; | |||
var bytesToRead = dataSize; | |||
readSize = 0; | |||
while (bytesToRead > 0) | |||
{ | |||
var bytesRead = stream.Read(readBuffer, 0, (int)Math.Min(readBuffer.Length, bytesToRead)); | |||
if (bytesRead == 0) | |||
{ | |||
break; | |||
} | |||
fixed (byte* readBufferPtr = readBuffer) | |||
{ | |||
Buffer.MemoryCopy(readBufferPtr, buffer + readSize, dataSize - readSize, bytesRead); | |||
} | |||
bytesToRead -= (uint)bytesRead; | |||
readSize += (uint)bytesRead; | |||
} | |||
} | |||
protected virtual void Dispose(bool disposing) | |||
{ | |||
if (!disposedValue) | |||
{ | |||
if (disposing) | |||
{ | |||
stream?.Dispose(); | |||
} | |||
disposedValue = true; | |||
} | |||
} | |||
// This does not have unmanagd resources, so it doesn't need to exist | |||
// ~AmsiFileStream() | |||
// { | |||
// // Do not change this code. Put cleanup code in 'Dispose(bool disposing)' method | |||
// Dispose(disposing: false); | |||
// } | |||
public void Dispose() | |||
{ | |||
// Do not change this code. Put cleanup code in 'Dispose(bool disposing)' method | |||
Dispose(disposing: true); | |||
GC.SuppressFinalize(this); | |||
} | |||
} | |||
} |
@ -0,0 +1,114 @@ | |||
#nullable enable | |||
using System; | |||
using System.Collections.Generic; | |||
using System.IO; | |||
using System.Linq; | |||
using System.Runtime.InteropServices; | |||
using System.Text; | |||
using System.Threading.Tasks; | |||
namespace IPA.AntiMalware.ComAPI | |||
{ | |||
internal class AmsiMemoryStream : IAmsiStream, IDisposable | |||
{ | |||
private readonly string contentName; | |||
private readonly byte[] data; | |||
private readonly GCHandle dataHandle; | |||
private readonly IntPtr session; | |||
private bool disposedValue; | |||
public AmsiMemoryStream(string contentName, byte[] data, IntPtr session) | |||
{ | |||
this.data = data; | |||
dataHandle = GCHandle.Alloc(data, GCHandleType.Pinned); | |||
this.session = session; | |||
this.contentName = contentName; | |||
} | |||
public unsafe void GetAttribute([In] AmsiAttribute attribute, [In] uint dataSize, [Out] byte* buffer, out uint writtenData) | |||
{ | |||
switch (attribute) | |||
{ | |||
case AmsiAttribute.AppName: | |||
writtenData = WriteWString(AmsiConstants.AppName, dataSize, buffer); | |||
return; | |||
case AmsiAttribute.Session: | |||
*(IntPtr*)buffer = session; | |||
writtenData = (uint)sizeof(IntPtr); | |||
return; | |||
case AmsiAttribute.ContentName: | |||
writtenData = WriteWString(contentName, dataSize, buffer); | |||
return; | |||
case AmsiAttribute.ContentSize: | |||
*(ulong*)buffer = (ulong)data.Length; | |||
writtenData = sizeof(ulong); | |||
return; | |||
case AmsiAttribute.ContentAddress: | |||
// because our data is pinned, it can't move while this object exists so we can pass out the fixed address | |||
fixed (byte* dataAddr = data) | |||
{ | |||
*(byte**)buffer = dataAddr; | |||
} | |||
writtenData = (uint)sizeof(IntPtr); | |||
return; | |||
default: | |||
throw new NotImplementedException(); // return e_notimpl | |||
} | |||
static unsafe uint WriteWString(string str, uint dataSize, byte* buffer) | |||
{ | |||
fixed (char* name = str) | |||
{ | |||
return (uint)Encoding.Unicode.GetBytes(name, str.Length, buffer, (int)dataSize); | |||
} | |||
} | |||
} | |||
public unsafe void Read([In] ulong position, [In] uint dataSize, [Out] byte* buffer, out uint readSize) | |||
{ | |||
if (position >= (ulong)data.Length) | |||
{ | |||
throw new EndOfStreamException(); | |||
} | |||
fixed (byte* dataPtr = data) | |||
{ | |||
var toRead = Math.Min((ulong)data.Length - position, dataSize); | |||
Buffer.MemoryCopy(dataPtr + position, buffer, dataSize, toRead); | |||
readSize = (uint)toRead; | |||
} | |||
} | |||
protected virtual void Dispose(bool disposing) | |||
{ | |||
if (!disposedValue) | |||
{ | |||
if (disposing) | |||
{ | |||
// no managed stae to dispose | |||
} | |||
dataHandle.Free(); | |||
disposedValue = true; | |||
} | |||
} | |||
~AmsiMemoryStream() | |||
{ | |||
// Do not change this code. Put cleanup code in 'Dispose(bool disposing)' method | |||
Dispose(disposing: false); | |||
} | |||
public void Dispose() | |||
{ | |||
// Do not change this code. Put cleanup code in 'Dispose(bool disposing)' method | |||
Dispose(disposing: true); | |||
GC.SuppressFinalize(this); | |||
} | |||
} | |||
} |
@ -0,0 +1,52 @@ | |||
#nullable enable | |||
using System; | |||
using System.Collections.Generic; | |||
using System.Linq; | |||
using System.Runtime.InteropServices; | |||
using System.Text; | |||
using System.Threading.Tasks; | |||
namespace IPA.AntiMalware.ComAPI | |||
{ | |||
[ComImport] | |||
[Guid(AmsiConstants.IAntimalwareGuidStr)] | |||
[InterfaceType(ComInterfaceType.InterfaceIsIUnknown)] | |||
internal interface IAntimalware | |||
{ | |||
void Scan([In] IAmsiStream stream, [Out] out AmsiResult result, [Out] out IAntimalwareProvider provider); | |||
void CloseSession([In] ulong session); | |||
} | |||
[ComImport] | |||
[Guid("3e47f2e5-81d4-4d3b-897f-545096770373")] | |||
[InterfaceType(ComInterfaceType.InterfaceIsIUnknown)] | |||
internal interface IAmsiStream | |||
{ | |||
unsafe void GetAttribute([In] AmsiAttribute attribute, [In] uint dataSize, [Out] byte* buffer, [Out] out uint writtenData); | |||
unsafe void Read([In] ulong position, [In] uint dataSize, [Out] byte* buffer, [Out] out uint readSize); | |||
} | |||
[ComImport] | |||
[Guid("b2cabfe3-fe04-42b1-a5df-08d483d4d125")] | |||
[InterfaceType(ComInterfaceType.InterfaceIsIUnknown)] | |||
internal interface IAntimalwareProvider | |||
{ | |||
[return: MarshalAs(UnmanagedType.LPWStr)] string DisplayName(); | |||
AmsiResult Scan([In] IAmsiStream stream); | |||
void CloseSession([In] ulong session); | |||
} | |||
internal enum AmsiAttribute | |||
{ | |||
AppName = 0, | |||
ContentName = 1, | |||
ContentSize = 2, | |||
ContentAddress = 3, | |||
Session = 4, | |||
RedirectChainSize = 5, | |||
RedirectChainAddress = 6, | |||
AllSize = 7, | |||
AllAddress = 8, | |||
} | |||
} |
@ -0,0 +1,88 @@ | |||
#nullable enable | |||
using IPA.AntiMalware.ComAPI; | |||
using IPA.Logging; | |||
using System; | |||
using System.Collections.Generic; | |||
using System.IO; | |||
using System.Linq; | |||
using System.Runtime.InteropServices; | |||
using System.Text; | |||
using System.Threading.Tasks; | |||
namespace IPA.AntiMalware | |||
{ | |||
internal class WindowsCOMAntiMalware : IAntiMalware | |||
{ | |||
internal static WindowsCOMAntiMalware? TryInitialize() | |||
{ | |||
// Mono's COM interop *fundamentally doesn't work.* | |||
// End of story. | |||
#if false | |||
try | |||
{ | |||
return new(); | |||
} | |||
catch (Exception e) | |||
{ | |||
Logger.AntiMalware.Warn("Could not initialize COM-based antimalware engine:"); | |||
Logger.AntiMalware.Warn(e); | |||
} | |||
#endif | |||
return null; | |||
} | |||
private readonly IAntimalware amInterface; | |||
private WindowsCOMAntiMalware() | |||
{ | |||
var hr = CoCreateInstanceAM(AmsiConstants.CAntimalwareGuid, | |||
null, | |||
0x1 | 0x4 /* inproc server, local server */, | |||
AmsiConstants.IAntimalwareGuid, | |||
out var antimalware); | |||
Marshal.ThrowExceptionForHR(hr); | |||
amInterface = antimalware; | |||
} | |||
[DllImport("ole32", | |||
CallingConvention = CallingConvention.Winapi, | |||
ExactSpelling = true, | |||
PreserveSig = false, | |||
EntryPoint = "CoCreateInstance")] | |||
[DefaultDllImportSearchPaths(DllImportSearchPath.System32)] | |||
private static extern int CoCreateInstanceAM( | |||
[In] in Guid clsid, | |||
[In, MarshalAs(UnmanagedType.Interface)] object? unkOuter, | |||
[In] int dwClsContext, | |||
[In] in Guid iid, | |||
[Out, MarshalAs(UnmanagedType.Interface)] out IAntimalware @interface); | |||
private static ScanResult ScanResultFromAmsiResult(AmsiResult result) | |||
=> result switch | |||
{ | |||
AmsiResult.Clean => ScanResult.KnownSafe, | |||
AmsiResult.NotDetected => ScanResult.NotDetected, | |||
AmsiResult.Detected => ScanResult.Detected, | |||
_ => ScanResult.MaybeMalware | |||
}; | |||
public ScanResult ScanFile(FileInfo file) | |||
{ | |||
using var stream = new AmsiFileStream(file, IntPtr.Zero); | |||
amInterface.Scan(stream, out var result, out var provider); | |||
Logger.AntiMalware.Trace($"Scanned file '{file}' with {provider.DisplayName()}, and got '{result}'"); | |||
return ScanResultFromAmsiResult(result); | |||
} | |||
public ScanResult ScanData(byte[] data, string? contentName = null) | |||
{ | |||
contentName ??= $"unknown_data_{Guid.NewGuid()}"; | |||
using var stream = new AmsiMemoryStream(contentName, data, IntPtr.Zero); | |||
amInterface.Scan(stream, out var result, out var provider); | |||
Logger.AntiMalware.Trace($"Scanned data named '{contentName}' with {provider.DisplayName()}, and got '{result}'"); | |||
return ScanResultFromAmsiResult(result); | |||
} | |||
} | |||
} |
@ -1,145 +1,146 @@ | |||
using System; | |||
using System.Collections; | |||
using System.Collections.Generic; | |||
using System.Linq; | |||
using System.Text; | |||
using System.Threading.Tasks; | |||
namespace IPA.Config.Data | |||
{ | |||
/// <summary> | |||
/// A base value type for config data abstract representations, to be serialized with an | |||
/// <see cref="IConfigProvider"/>. If a <see cref="Value"/> is <see langword="null"/>, then | |||
/// that represents just that: a <c>null</c> in whatever serialization is being used. | |||
/// Also contains factory functions for all derived types. | |||
/// </summary> | |||
public abstract class Value | |||
{ | |||
/// <summary> | |||
/// Converts this <see cref="Value"/> into a human-readable format. | |||
/// </summary> | |||
/// <returns>a human-readable string containing the value provided</returns> | |||
public abstract override string ToString(); | |||
/// <summary> | |||
/// Creates a Null <see cref="Value"/>. | |||
/// </summary> | |||
/// <returns><see langword="null"/></returns> | |||
public static Value Null() => null; | |||
/// <summary> | |||
/// Creates an empty <see cref="List"/>. | |||
/// </summary> | |||
/// <returns>an empty <see cref="List"/></returns> | |||
/// <seealso cref="From(IEnumerable{Value})"/> | |||
public static List List() => new List(); | |||
/// <summary> | |||
/// Creates an empty <see cref="Map"/>. | |||
/// </summary> | |||
/// <returns>an empty <see cref="Map"/></returns> | |||
/// <seealso cref="From(IDictionary{string, Value})"/> | |||
/// <seealso cref="From(IEnumerable{KeyValuePair{string, Value}})"/> | |||
public static Map Map() => new Map(); | |||
/// <summary> | |||
/// Creates a new <see cref="Value"/> representing a <see cref="string"/>. | |||
/// </summary> | |||
/// <param name="val">the value to wrap</param> | |||
/// <returns>a <see cref="Data.Text"/> wrapping <paramref name="val"/></returns> | |||
/// <seealso cref="Text(string)"/> | |||
public static Text From(string val) => Text(val); | |||
/// <summary> | |||
/// Creates a new <see cref="Data.Text"/> object wrapping a <see cref="string"/>. | |||
/// </summary> | |||
/// <param name="val">the value to wrap</param> | |||
/// <returns>a <see cref="Data.Text"/> wrapping <paramref name="val"/></returns> | |||
/// <seealso cref="From(string)"/> | |||
public static Text Text(string val) => val == null ? null : new Text { Value = val }; | |||
/// <summary> | |||
/// Creates a new <see cref="Value"/> wrapping a <see cref="long"/>. | |||
/// </summary> | |||
/// <param name="val">the value to wrap</param> | |||
/// <returns>a <see cref="Data.Integer"/> wrapping <paramref name="val"/></returns> | |||
/// <seealso cref="Integer(long)"/> | |||
public static Integer From(long val) => Integer(val); | |||
/// <summary> | |||
/// Creates a new <see cref="Data.Integer"/> wrapping a <see cref="long"/>. | |||
/// </summary> | |||
/// <param name="val">the value to wrap</param> | |||
/// <returns>a <see cref="Data.Integer"/> wrapping <paramref name="val"/></returns> | |||
/// <seealso cref="From(long)"/> | |||
public static Integer Integer(long val) => new Integer { Value = val }; | |||
/// <summary> | |||
/// Creates a new <see cref="Value"/> wrapping a <see cref="double"/>. | |||
/// </summary> | |||
/// <param name="val">the value to wrap</param> | |||
/// <returns>a <see cref="FloatingPoint"/> wrapping <paramref name="val"/></returns> | |||
/// <seealso cref="Float(decimal)"/> | |||
public static FloatingPoint From(decimal val) => Float(val); | |||
/// <summary> | |||
/// Creates a new <see cref="FloatingPoint"/> wrapping a <see cref="decimal"/>. | |||
/// </summary> | |||
/// <param name="val">the value to wrap</param> | |||
/// <returns>a <see cref="FloatingPoint"/> wrapping <paramref name="val"/></returns> | |||
/// <seealso cref="From(decimal)"/> | |||
public static FloatingPoint Float(decimal val) => new FloatingPoint { Value = val }; | |||
/// <summary> | |||
/// Creates a new <see cref="Value"/> wrapping a <see cref="bool"/>. | |||
/// </summary> | |||
/// <param name="val">the value to wrap</param> | |||
/// <returns>a <see cref="Boolean"/> wrapping <paramref name="val"/></returns> | |||
/// <seealso cref="Bool(bool)"/> | |||
public static Boolean From(bool val) => Bool(val); | |||
/// <summary> | |||
/// Creates a new <see cref="Boolean"/> wrapping a <see cref="bool"/>. | |||
/// </summary> | |||
/// <param name="val">the value to wrap</param> | |||
/// <returns>a <see cref="Boolean"/> wrapping <paramref name="val"/></returns> | |||
/// <seealso cref="From(bool)"/> | |||
public static Boolean Bool(bool val) => new Boolean { Value = val }; | |||
/// <summary> | |||
/// Creates a new <see cref="Data.List"/> holding the content of an <see cref="IEnumerable{T}"/> | |||
/// of <see cref="Value"/>. | |||
/// </summary> | |||
/// <param name="vals">the <see cref="Value"/>s to initialize the <see cref="Data.List"/> with</param> | |||
/// <returns>a <see cref="Data.List"/> containing the content of <paramref name="vals"/></returns> | |||
/// <seealso cref="List"/> | |||
public static List From(IEnumerable<Value> vals) | |||
{ | |||
if (vals == null) return null; | |||
var l = List(); | |||
l.AddRange(vals); | |||
return l; | |||
} | |||
/// <summary> | |||
/// Creates a new <see cref="Data.Map"/> holding the content of an <see cref="IDictionary{TKey, TValue}"/> | |||
/// of <see cref="string"/> to <see cref="Value"/>. | |||
/// </summary> | |||
/// <param name="vals">the dictionary of <see cref="Value"/>s to initialize the <see cref="Data.Map"/> wtih</param> | |||
/// <returns>a <see cref="Data.Map"/> containing the content of <paramref name="vals"/></returns> | |||
/// <seealso cref="Map"/> | |||
/// <seealso cref="From(IEnumerable{KeyValuePair{string, Value}})"/> | |||
public static Map From(IDictionary<string, Value> vals) => From(vals as IEnumerable<KeyValuePair<string, Value>>); | |||
/// <summary> | |||
/// Creates a new <see cref="Data.Map"/> holding the content of an <see cref="IEnumerable{T}"/> | |||
/// of <see cref="KeyValuePair{TKey, TValue}"/> of <see cref="string"/> to <see cref="Value"/>. | |||
/// </summary> | |||
/// <param name="vals">the enumerable of <see cref="KeyValuePair{TKey, TValue}"/> of name to <see cref="Value"/></param> | |||
/// <returns>a <see cref="Data.Map"/> containing the content of <paramref name="vals"/></returns> | |||
/// <seealso cref="Map"/> | |||
/// <seealso cref="From(IDictionary{string, Value})"/> | |||
public static Map From(IEnumerable<KeyValuePair<string, Value>> vals) | |||
{ | |||
if (vals == null) return null; | |||
var m = Map(); | |||
foreach (var v in vals) m.Add(v.Key, v.Value); | |||
return m; | |||
} | |||
} | |||
} | |||
#nullable enable | |||
using System.Collections.Generic; | |||
using System.Diagnostics.CodeAnalysis; | |||
namespace IPA.Config.Data | |||
{ | |||
/// <summary> | |||
/// A base value type for config data abstract representations, to be serialized with an | |||
/// <see cref="IConfigProvider"/>. If a <see cref="Value"/> is <see langword="null"/>, then | |||
/// that represents just that: a <c>null</c> in whatever serialization is being used. | |||
/// Also contains factory functions for all derived types. | |||
/// </summary> | |||
public abstract class Value | |||
{ | |||
/// <summary> | |||
/// Converts this <see cref="Value"/> into a human-readable format. | |||
/// </summary> | |||
/// <returns>a human-readable string containing the value provided</returns> | |||
public abstract override string ToString(); | |||
/// <summary> | |||
/// Creates a Null <see cref="Value"/>. | |||
/// </summary> | |||
/// <returns><see langword="null"/></returns> | |||
public static Value? Null() => null; | |||
/// <summary> | |||
/// Creates an empty <see cref="List"/>. | |||
/// </summary> | |||
/// <returns>an empty <see cref="List"/></returns> | |||
/// <seealso cref="From(IEnumerable{Value})"/> | |||
public static List List() => new(); | |||
/// <summary> | |||
/// Creates an empty <see cref="Map"/>. | |||
/// </summary> | |||
/// <returns>an empty <see cref="Map"/></returns> | |||
/// <seealso cref="From(IDictionary{string, Value})"/> | |||
/// <seealso cref="From(IEnumerable{KeyValuePair{string, Value}})"/> | |||
public static Map Map() => new(); | |||
/// <summary> | |||
/// Creates a new <see cref="Value"/> representing a <see cref="string"/>. | |||
/// </summary> | |||
/// <param name="val">the value to wrap</param> | |||
/// <returns>a <see cref="Data.Text"/> wrapping <paramref name="val"/></returns> | |||
/// <seealso cref="Text(string)"/> | |||
[return: NotNullIfNotNull("val")] | |||
public static Text? From(string? val) => Text(val); | |||
/// <summary> | |||
/// Creates a new <see cref="Data.Text"/> object wrapping a <see cref="string"/>. | |||
/// </summary> | |||
/// <param name="val">the value to wrap</param> | |||
/// <returns>a <see cref="Data.Text"/> wrapping <paramref name="val"/></returns> | |||
/// <seealso cref="From(string)"/> | |||
[return: NotNullIfNotNull("val")] | |||
public static Text? Text(string? val) => val == null ? null : new(val); | |||
/// <summary> | |||
/// Creates a new <see cref="Value"/> wrapping a <see cref="long"/>. | |||
/// </summary> | |||
/// <param name="val">the value to wrap</param> | |||
/// <returns>a <see cref="Data.Integer"/> wrapping <paramref name="val"/></returns> | |||
/// <seealso cref="Integer(long)"/> | |||
public static Integer From(long val) => Integer(val); | |||
/// <summary> | |||
/// Creates a new <see cref="Data.Integer"/> wrapping a <see cref="long"/>. | |||
/// </summary> | |||
/// <param name="val">the value to wrap</param> | |||
/// <returns>a <see cref="Data.Integer"/> wrapping <paramref name="val"/></returns> | |||
/// <seealso cref="From(long)"/> | |||
public static Integer Integer(long val) => new(val); | |||
/// <summary> | |||
/// Creates a new <see cref="Value"/> wrapping a <see cref="double"/>. | |||
/// </summary> | |||
/// <param name="val">the value to wrap</param> | |||
/// <returns>a <see cref="FloatingPoint"/> wrapping <paramref name="val"/></returns> | |||
/// <seealso cref="Float(decimal)"/> | |||
public static FloatingPoint From(decimal val) => Float(val); | |||
/// <summary> | |||
/// Creates a new <see cref="FloatingPoint"/> wrapping a <see cref="decimal"/>. | |||
/// </summary> | |||
/// <param name="val">the value to wrap</param> | |||
/// <returns>a <see cref="FloatingPoint"/> wrapping <paramref name="val"/></returns> | |||
/// <seealso cref="From(decimal)"/> | |||
public static FloatingPoint Float(decimal val) => new(val); | |||
/// <summary> | |||
/// Creates a new <see cref="Value"/> wrapping a <see cref="bool"/>. | |||
/// </summary> | |||
/// <param name="val">the value to wrap</param> | |||
/// <returns>a <see cref="Boolean"/> wrapping <paramref name="val"/></returns> | |||
/// <seealso cref="Bool(bool)"/> | |||
public static Boolean From(bool val) => Bool(val); | |||
/// <summary> | |||
/// Creates a new <see cref="Boolean"/> wrapping a <see cref="bool"/>. | |||
/// </summary> | |||
/// <param name="val">the value to wrap</param> | |||
/// <returns>a <see cref="Boolean"/> wrapping <paramref name="val"/></returns> | |||
/// <seealso cref="From(bool)"/> | |||
public static Boolean Bool(bool val) => new(val); | |||
/// <summary> | |||
/// Creates a new <see cref="Data.List"/> holding the content of an <see cref="IEnumerable{T}"/> | |||
/// of <see cref="Value"/>. | |||
/// </summary> | |||
/// <param name="vals">the <see cref="Value"/>s to initialize the <see cref="Data.List"/> with</param> | |||
/// <returns>a <see cref="Data.List"/> containing the content of <paramref name="vals"/></returns> | |||
/// <seealso cref="List"/> | |||
[return: NotNullIfNotNull("vals")] | |||
public static List? From(IEnumerable<Value?>? vals) | |||
{ | |||
if (vals is null) return null; | |||
var l = List(); | |||
l.AddRange(vals); | |||
return l; | |||
} | |||
/// <summary> | |||
/// Creates a new <see cref="Data.Map"/> holding the content of an <see cref="IDictionary{TKey, TValue}"/> | |||
/// of <see cref="string"/> to <see cref="Value"/>. | |||
/// </summary> | |||
/// <param name="vals">the dictionary of <see cref="Value"/>s to initialize the <see cref="Data.Map"/> wtih</param> | |||
/// <returns>a <see cref="Data.Map"/> containing the content of <paramref name="vals"/></returns> | |||
/// <seealso cref="Map"/> | |||
/// <seealso cref="From(IEnumerable{KeyValuePair{string, Value}})"/> | |||
public static Map From(IDictionary<string, Value?> vals) => From(vals as IEnumerable<KeyValuePair<string, Value?>>); | |||
/// <summary> | |||
/// Creates a new <see cref="Data.Map"/> holding the content of an <see cref="IEnumerable{T}"/> | |||
/// of <see cref="KeyValuePair{TKey, TValue}"/> of <see cref="string"/> to <see cref="Value"/>. | |||
/// </summary> | |||
/// <param name="vals">the enumerable of <see cref="KeyValuePair{TKey, TValue}"/> of name to <see cref="Value"/></param> | |||
/// <returns>a <see cref="Data.Map"/> containing the content of <paramref name="vals"/></returns> | |||
/// <seealso cref="Map"/> | |||
/// <seealso cref="From(IDictionary{string, Value})"/> | |||
[return: NotNullIfNotNull("vals")] | |||
public static Map? From(IEnumerable<KeyValuePair<string, Value?>>? vals) | |||
{ | |||
if (vals is null) return null; | |||
var m = Map(); | |||
foreach (var v in vals) m.Add(v.Key, v.Value); | |||
return m; | |||
} | |||
} | |||
} |
@ -1,15 +1,20 @@ | |||
using System; | |||
using System.Diagnostics.CodeAnalysis; | |||
using Newtonsoft.Json; | |||
using SemVer; | |||
namespace IPA.JsonConverters | |||
{ | |||
[SuppressMessage("ReSharper", "UnusedMember.Global")] | |||
internal class SemverRangeConverter : JsonConverter<Range> | |||
{ | |||
public override Range ReadJson(JsonReader reader, Type objectType, Range existingValue, bool hasExistingValue, JsonSerializer serializer) => new Range(reader.Value as string); | |||
public override void WriteJson(JsonWriter writer, Range value, JsonSerializer serializer) => writer.WriteValue(value.ToString()); | |||
} | |||
} | |||
#nullable enable | |||
using System; | |||
using System.Runtime.Remoting.Messaging; | |||
using Hive.Versioning; | |||
using Newtonsoft.Json; | |||
namespace IPA.JsonConverters | |||
{ | |||
internal class SemverRangeConverter : JsonConverter<VersionRange?> | |||
{ | |||
public override VersionRange? ReadJson(JsonReader reader, Type objectType, VersionRange? existingValue, bool hasExistingValue, JsonSerializer serializer) | |||
=> reader.Value is not string s ? existingValue : new VersionRange(s); | |||
public override void WriteJson(JsonWriter writer, VersionRange? value, JsonSerializer serializer) | |||
{ | |||
if (value is null) writer.WriteNull(); | |||
else writer.WriteValue(value.ToString()); | |||
} | |||
} | |||
} |
@ -1,17 +1,19 @@ | |||
using System; | |||
using Newtonsoft.Json; | |||
using Version = SemVer.Version; | |||
namespace IPA.JsonConverters | |||
{ | |||
internal class SemverVersionConverter : JsonConverter<Version> | |||
{ | |||
public override Version ReadJson(JsonReader reader, Type objectType, Version existingValue, bool hasExistingValue, JsonSerializer serializer) => reader.Value == null ? null : new Version(reader.Value as string, true); | |||
public override void WriteJson(JsonWriter writer, Version value, JsonSerializer serializer) | |||
{ | |||
if (value == null) writer.WriteNull(); | |||
else writer.WriteValue(value.ToString()); | |||
} | |||
} | |||
} | |||
#nullable enable | |||
using System; | |||
using Newtonsoft.Json; | |||
using Version = Hive.Versioning.Version; | |||
namespace IPA.JsonConverters | |||
{ | |||
internal class SemverVersionConverter : JsonConverter<Version?> | |||
{ | |||
public override Version? ReadJson(JsonReader reader, Type objectType, Version? existingValue, bool hasExistingValue, JsonSerializer serializer) | |||
=> reader.Value is not string s ? existingValue : new Version(s); | |||
public override void WriteJson(JsonWriter writer, Version? value, JsonSerializer serializer) | |||
{ | |||
if (value == null) writer.WriteNull(); | |||
else writer.WriteValue(value.ToString()); | |||
} | |||
} | |||
} |
@ -0,0 +1,21 @@ | |||
using System; | |||
using System.Diagnostics.CodeAnalysis; | |||
namespace IPA.Loader | |||
{ | |||
[SuppressMessage("Design", "CA1064:Exceptions should be public", Justification = "This is only thrown and caught in local code")] | |||
internal sealed class DependencyResolutionLoopException : Exception | |||
{ | |||
public DependencyResolutionLoopException(string message) : base(message) | |||
{ | |||
} | |||
public DependencyResolutionLoopException(string message, Exception innerException) : base(message, innerException) | |||
{ | |||
} | |||
public DependencyResolutionLoopException() | |||
{ | |||
} | |||
} | |||
} |
@ -1,248 +1,237 @@ | |||
using System; | |||
using System.Collections.Generic; | |||
using System.ComponentModel; | |||
using System.Diagnostics.CodeAnalysis; | |||
using System.IO; | |||
using System.Reflection; | |||
using System.Runtime.InteropServices; | |||
using System.Linq; | |||
using IPA.Logging; | |||
using IPA.Utilities; | |||
using Mono.Cecil; | |||
#if NET3 | |||
using Net3_Proxy; | |||
using Directory = Net3_Proxy.Directory; | |||
using Path = Net3_Proxy.Path; | |||
using File = Net3_Proxy.File; | |||
#endif | |||
namespace IPA.Loader | |||
{ | |||
internal class CecilLibLoader : BaseAssemblyResolver | |||
{ | |||
private static readonly string CurrentAssemblyName = Assembly.GetExecutingAssembly().GetName().Name; | |||
private static readonly string CurrentAssemblyPath = Assembly.GetExecutingAssembly().Location; | |||
public override AssemblyDefinition Resolve(AssemblyNameReference name, ReaderParameters parameters) | |||
{ | |||
LibLoader.SetupAssemblyFilenames(); | |||
if (name.Name == CurrentAssemblyName) | |||
return AssemblyDefinition.ReadAssembly(CurrentAssemblyPath, parameters); | |||
if (LibLoader.FilenameLocations.TryGetValue($"{name.Name}.dll", out var path)) | |||
{ | |||
if (File.Exists(path)) | |||
return AssemblyDefinition.ReadAssembly(path, parameters); | |||
} | |||
else if (LibLoader.FilenameLocations.TryGetValue($"{name.Name}.{name.Version}.dll", out path)) | |||
{ | |||
if (File.Exists(path)) | |||
return AssemblyDefinition.ReadAssembly(path, parameters); | |||
} | |||
return base.Resolve(name, parameters); | |||
} | |||
} | |||
internal static class LibLoader | |||
{ | |||
internal static string LibraryPath => Path.Combine(Environment.CurrentDirectory, "Libs"); | |||
internal static string NativeLibraryPath => Path.Combine(LibraryPath, "Native"); | |||
internal static Dictionary<string, string> FilenameLocations; | |||
internal static void Configure() | |||
{ | |||
SetupAssemblyFilenames(true); | |||
AppDomain.CurrentDomain.AssemblyResolve -= AssemblyLibLoader; | |||
AppDomain.CurrentDomain.AssemblyResolve += AssemblyLibLoader; | |||
} | |||
internal static void SetupAssemblyFilenames(bool force = false) | |||
{ | |||
if (FilenameLocations == null || force) | |||
{ | |||
FilenameLocations = new Dictionary<string, string>(); | |||
#nullable enable | |||
using System; | |||
using System.Collections.Generic; | |||
using System.Diagnostics.CodeAnalysis; | |||
using System.IO; | |||
using System.Reflection; | |||
using System.Linq; | |||
using IPA.Logging; | |||
using IPA.Utilities; | |||
using Mono.Cecil; | |||
using IPA.AntiMalware; | |||
using IPA.Config; | |||
#if NET3 | |||
using Net3_Proxy; | |||
using Directory = Net3_Proxy.Directory; | |||
using Path = Net3_Proxy.Path; | |||
using File = Net3_Proxy.File; | |||
#endif | |||
namespace IPA.Loader | |||
{ | |||
internal class CecilLibLoader : BaseAssemblyResolver | |||
{ | |||
private static readonly string CurrentAssemblyName = Assembly.GetExecutingAssembly().GetName().Name; | |||
private static readonly string CurrentAssemblyPath = Assembly.GetExecutingAssembly().Location; | |||
public override AssemblyDefinition Resolve(AssemblyNameReference name, ReaderParameters parameters) | |||
{ | |||
LibLoader.SetupAssemblyFilenames(); | |||
if (name.Name == CurrentAssemblyName) | |||
return AssemblyDefinition.ReadAssembly(CurrentAssemblyPath, parameters); | |||
if (LibLoader.FilenameLocations.TryGetValue($"{name.Name}.dll", out var path)) | |||
{ | |||
if (File.Exists(path)) | |||
return AssemblyDefinition.ReadAssembly(path, parameters); | |||
} | |||
else if (LibLoader.FilenameLocations.TryGetValue($"{name.Name}.{name.Version}.dll", out path)) | |||
{ | |||
if (File.Exists(path)) | |||
return AssemblyDefinition.ReadAssembly(path, parameters); | |||
} | |||
return base.Resolve(name, parameters); | |||
} | |||
} | |||
internal static class LibLoader | |||
{ | |||
internal static string LibraryPath => Path.Combine(Environment.CurrentDirectory, "Libs"); | |||
internal static string NativeLibraryPath => Path.Combine(LibraryPath, "Native"); | |||
internal static Dictionary<string, string> FilenameLocations = null!; | |||
internal static void Configure() | |||
{ | |||
SetupAssemblyFilenames(true); | |||
AppDomain.CurrentDomain.AssemblyResolve -= AssemblyLibLoader; | |||
AppDomain.CurrentDomain.AssemblyResolve += AssemblyLibLoader; | |||
} | |||
internal static void SetupAssemblyFilenames(bool force = false) | |||
{ | |||
if (FilenameLocations == null || force) | |||
{ | |||
FilenameLocations = new Dictionary<string, string>(); | |||
foreach (var fn in TraverseTree(LibraryPath, s => s != NativeLibraryPath)) | |||
{ | |||
if (FilenameLocations.ContainsKey(fn.Name)) | |||
Log(Logger.Level.Critical, $"Multiple instances of {fn.Name} exist in Libs! Ignoring {fn.FullName}"); | |||
if (FilenameLocations.ContainsKey(fn.Name)) | |||
Log(Logger.Level.Critical, $"Multiple instances of {fn.Name} exist in Libs! Ignoring {fn.FullName}"); | |||
else FilenameLocations.Add(fn.Name, fn.FullName); | |||
} | |||
if (!SetDefaultDllDirectories(LoadLibraryFlags.LOAD_LIBRARY_SEARCH_USER_DIRS | LoadLibraryFlags.LOAD_LIBRARY_SEARCH_SYSTEM32 | |||
| LoadLibraryFlags.LOAD_LIBRARY_SEARCH_DEFAULT_DIRS | LoadLibraryFlags.LOAD_LIBRARY_SEARCH_APPLICATION_DIR)) | |||
{ | |||
var err = new Win32Exception(); | |||
Log(Logger.Level.Critical, $"Error configuring DLL search path"); | |||
Log(Logger.Level.Critical, err); | |||
return; | |||
} | |||
static void AddDir(string path) | |||
{ | |||
var retPtr = AddDllDirectory(path); | |||
if (retPtr == IntPtr.Zero) | |||
{ | |||
var err = new Win32Exception(); | |||
Log(Logger.Level.Warning, $"Could not add DLL directory {path}"); | |||
Log(Logger.Level.Warning, err); | |||
} | |||
} | |||
if (Directory.Exists(NativeLibraryPath)) | |||
{ | |||
AddDir(NativeLibraryPath); | |||
_ = TraverseTree(NativeLibraryPath, dir => | |||
{ // this is a terrible hack for iterating directories | |||
AddDir(dir); return true; | |||
}).All(f => true); // force it to iterate all | |||
} | |||
//var unityData = Directory.EnumerateDirectories(Environment.CurrentDirectory, "*_Data").First(); | |||
//AddDir(Path.Combine(unityData, "Plugins")); | |||
foreach (var dir in Environment.GetEnvironmentVariable("path") | |||
.Split(Path.PathSeparator) | |||
.Select(Environment.ExpandEnvironmentVariables)) | |||
} | |||
static void AddDir(string path) | |||
{ | |||
AddDir(dir); | |||
} | |||
} | |||
} | |||
public static Assembly AssemblyLibLoader(object source, ResolveEventArgs e) | |||
{ | |||
var asmName = new AssemblyName(e.Name); | |||
return LoadLibrary(asmName); | |||
} | |||
internal static Assembly LoadLibrary(AssemblyName asmName) | |||
{ | |||
Log(Logger.Level.Debug, $"Resolving library {asmName}"); | |||
SetupAssemblyFilenames(); | |||
var testFile = $"{asmName.Name}.dll"; | |||
Log(Logger.Level.Debug, $"Looking for file {asmName.Name}.dll"); | |||
if (FilenameLocations.TryGetValue(testFile, out var path)) | |||
{ | |||
Log(Logger.Level.Debug, $"Found file {testFile} as {path}"); | |||
if (File.Exists(path)) | |||
return Assembly.LoadFrom(path); | |||
Log(Logger.Level.Critical, $"but {path} no longer exists!"); | |||
} | |||
else if (FilenameLocations.TryGetValue(testFile = $"{asmName.Name}.{asmName.Version}.dll", out path)) | |||
{ | |||
Log(Logger.Level.Debug, $"Found file {testFile} as {path}"); | |||
Log(Logger.Level.Warning, $"File {testFile} should be renamed to just {asmName.Name}.dll"); | |||
if (File.Exists(path)) | |||
return Assembly.LoadFrom(path); | |||
Log(Logger.Level.Critical, $"but {path} no longer exists!"); | |||
} | |||
Log(Logger.Level.Critical, $"No library {asmName} found"); | |||
return null; | |||
} | |||
internal static void Log(Logger.Level lvl, string message) | |||
{ // multiple proxy methods to delay loading of assemblies until it's done | |||
if (Logger.LogCreated) | |||
AssemblyLibLoaderCallLogger(lvl, message); | |||
else | |||
if (((byte)lvl & (byte)StandardLogger.PrintFilter) != 0) | |||
Console.WriteLine($"[{lvl}] {message}"); | |||
} | |||
internal static void Log(Logger.Level lvl, Exception message) | |||
{ // multiple proxy methods to delay loading of assemblies until it's done | |||
if (Logger.LogCreated) | |||
AssemblyLibLoaderCallLogger(lvl, message); | |||
else | |||
if (((byte)lvl & (byte)StandardLogger.PrintFilter) != 0) | |||
Console.WriteLine($"[{lvl}] {message}"); | |||
} | |||
private static void AssemblyLibLoaderCallLogger(Logger.Level lvl, string message) => Logger.libLoader.Log(lvl, message); | |||
private static void AssemblyLibLoaderCallLogger(Logger.Level lvl, Exception message) => Logger.libLoader.Log(lvl, message); | |||
// https://docs.microsoft.com/en-us/dotnet/csharp/programming-guide/file-system/how-to-iterate-through-a-directory-tree | |||
private static IEnumerable<FileInfo> TraverseTree(string root, Func<string, bool> dirValidator = null) | |||
{ | |||
if (dirValidator == null) dirValidator = s => true; | |||
var dirs = new Stack<string>(32); | |||
if (!Directory.Exists(root)) | |||
throw new ArgumentException("Directory does not exist", nameof(root)); | |||
dirs.Push(root); | |||
while (dirs.Count > 0) | |||
{ | |||
string currentDir = dirs.Pop(); | |||
string[] subDirs; | |||
try | |||
{ | |||
subDirs = Directory.GetDirectories(currentDir); | |||
} | |||
catch (UnauthorizedAccessException) | |||
{ continue; } | |||
catch (DirectoryNotFoundException) | |||
{ continue; } | |||
string[] files; | |||
try | |||
{ | |||
files = Directory.GetFiles(currentDir); | |||
} | |||
catch (UnauthorizedAccessException) | |||
{ continue; } | |||
catch (DirectoryNotFoundException) | |||
{ continue; } | |||
foreach (string str in subDirs) | |||
if (dirValidator(str)) dirs.Push(str); | |||
foreach (string file in files) | |||
{ | |||
FileInfo nextValue; | |||
try | |||
{ | |||
nextValue = new FileInfo(file); | |||
} | |||
catch (FileNotFoundException) | |||
{ continue; } | |||
yield return nextValue; | |||
} | |||
} | |||
} | |||
[DllImport("kernel32.dll", CharSet = CharSet.Unicode, SetLastError = true)] | |||
#if NET461 | |||
[DefaultDllImportSearchPaths(DllImportSearchPath.System32)] | |||
#endif | |||
private static extern IntPtr AddDllDirectory(string lpPathName); | |||
[Flags] | |||
private enum LoadLibraryFlags : uint | |||
{ | |||
None = 0, | |||
LOAD_LIBRARY_SEARCH_APPLICATION_DIR = 0x00000200, | |||
LOAD_LIBRARY_SEARCH_DEFAULT_DIRS = 0x00001000, | |||
LOAD_LIBRARY_SEARCH_SYSTEM32 = 0x00000800, | |||
LOAD_LIBRARY_SEARCH_USER_DIRS = 0x00000400, | |||
} | |||
[DllImport("kernel32.dll", SetLastError = true)] | |||
#if NET461 | |||
[DefaultDllImportSearchPaths(DllImportSearchPath.System32)] | |||
#endif | |||
private static extern bool SetDefaultDllDirectories(LoadLibraryFlags dwFlags); | |||
} | |||
} | |||
var pathEnvironmentVariable = Environment.GetEnvironmentVariable("Path"); | |||
Environment.SetEnvironmentVariable("Path", path + Path.PathSeparator + pathEnvironmentVariable); | |||
} | |||
if (Directory.Exists(NativeLibraryPath)) | |||
{ | |||
AddDir(NativeLibraryPath); | |||
_ = TraverseTree(NativeLibraryPath, dir => | |||
{ // this is a terrible hack for iterating directories | |||
AddDir(dir); return true; | |||
}).All(f => true); // force it to iterate all | |||
} | |||
//var unityData = Directory.EnumerateDirectories(Environment.CurrentDirectory, "*_Data").First(); | |||
//AddDir(Path.Combine(unityData, "Plugins")); | |||
// TODO: find a way to either safely remove Newtonsoft, or switch to a different JSON lib | |||
_ = LoadLibrary(new AssemblyName("Newtonsoft.Json, Version=12.0.0.0, Culture=neutral")); | |||
} | |||
} | |||
public static Assembly? AssemblyLibLoader(object source, ResolveEventArgs e) | |||
{ | |||
var asmName = new AssemblyName(e.Name); | |||
return LoadLibrary(asmName); | |||
} | |||
internal static Assembly? LoadLibrary(AssemblyName asmName) | |||
{ | |||
Log(Logger.Level.Debug, $"Resolving library {asmName}"); | |||
SetupAssemblyFilenames(); | |||
var testFile = $"{asmName.Name}.dll"; | |||
Log(Logger.Level.Debug, $"Looking for file {asmName.Name}.dll"); | |||
if (FilenameLocations.TryGetValue(testFile, out var path)) | |||
{ | |||
Log(Logger.Level.Debug, $"Found file {testFile} as {path}"); | |||
return LoadSafe(path); | |||
} | |||
else if (FilenameLocations.TryGetValue(testFile = $"{asmName.Name}.{asmName.Version}.dll", out path)) | |||
{ | |||
Log(Logger.Level.Debug, $"Found file {testFile} as {path}"); | |||
Log(Logger.Level.Warning, $"File {testFile} should be renamed to just {asmName.Name}.dll"); | |||
return LoadSafe(path); | |||
} | |||
Log(Logger.Level.Critical, $"No library {asmName} found"); | |||
return null; | |||
} | |||
private static Assembly? LoadSafe(string path) | |||
{ | |||
if (!File.Exists(path)) | |||
{ | |||
Log(Logger.Level.Critical, $"{path} no longer exists!"); | |||
return null; | |||
} | |||
if (AntiMalwareEngine.IsInitialized) | |||
{ | |||
var result = AntiMalwareEngine.Engine.ScanFile(new FileInfo(path)); | |||
if (result is ScanResult.Detected) | |||
{ | |||
Log(Logger.Level.Error, $"Scan of '{path}' found malware; not loading"); | |||
return null; | |||
} | |||
if (!SelfConfig.AntiMalware_.RunPartialThreatCode_ && result is not ScanResult.KnownSafe and not ScanResult.NotDetected) | |||
{ | |||
Log(Logger.Level.Error, $"Scan of '{path}' found partial threat; not loading. To load this, enable AntiMalware.RunPartialThreatCode in the config."); | |||
return null; | |||
} | |||
} | |||
return Assembly.LoadFrom(path); | |||
} | |||
internal static void Log(Logger.Level lvl, string message) | |||
{ // multiple proxy methods to delay loading of assemblies until it's done | |||
if (Logger.LogCreated) | |||
{ | |||
AssemblyLibLoaderCallLogger(lvl, message); | |||
} | |||
else | |||
{ | |||
if (((byte)lvl & (byte)StandardLogger.PrintFilter) != 0) | |||
Console.WriteLine($"[{lvl}] {message}"); | |||
} | |||
} | |||
internal static void Log(Logger.Level lvl, Exception message) | |||
{ // multiple proxy methods to delay loading of assemblies until it's done | |||
if (Logger.LogCreated) | |||
{ | |||
AssemblyLibLoaderCallLogger(lvl, message); | |||
} | |||
else | |||
{ | |||
if (((byte)lvl & (byte)StandardLogger.PrintFilter) != 0) | |||
Console.WriteLine($"[{lvl}] {message}"); | |||
} | |||
} | |||
private static void AssemblyLibLoaderCallLogger(Logger.Level lvl, string message) => Logger.LibLoader.Log(lvl, message); | |||
private static void AssemblyLibLoaderCallLogger(Logger.Level lvl, Exception message) => Logger.LibLoader.Log(lvl, message); | |||
// https://docs.microsoft.com/en-us/dotnet/csharp/programming-guide/file-system/how-to-iterate-through-a-directory-tree | |||
private static IEnumerable<FileInfo> TraverseTree(string root, Func<string, bool>? dirValidator = null) | |||
{ | |||
if (dirValidator == null) dirValidator = s => true; | |||
var dirs = new Stack<string>(32); | |||
if (!Directory.Exists(root)) | |||
throw new ArgumentException("Directory does not exist", nameof(root)); | |||
dirs.Push(root); | |||
while (dirs.Count > 0) | |||
{ | |||
string currentDir = dirs.Pop(); | |||
string[] subDirs; | |||
try | |||
{ | |||
subDirs = Directory.GetDirectories(currentDir); | |||
} | |||
catch (UnauthorizedAccessException) | |||
{ continue; } | |||
catch (DirectoryNotFoundException) | |||
{ continue; } | |||
string[] files; | |||
try | |||
{ | |||
files = Directory.GetFiles(currentDir); | |||
} | |||
catch (UnauthorizedAccessException) | |||
{ continue; } | |||
catch (DirectoryNotFoundException) | |||
{ continue; } | |||
foreach (string str in subDirs) | |||
if (dirValidator(str)) dirs.Push(str); | |||
foreach (string file in files) | |||
{ | |||
FileInfo nextValue; | |||
try | |||
{ | |||
nextValue = new FileInfo(file); | |||
} | |||
catch (FileNotFoundException) | |||
{ continue; } | |||
yield return nextValue; | |||
} | |||
} | |||
} | |||
} | |||
} |
@ -1,39 +1,40 @@ | |||
using System; | |||
namespace IPA.Logging | |||
{ | |||
/// <summary> | |||
/// The log printer's base class. | |||
/// </summary> | |||
public abstract class LogPrinter | |||
{ | |||
/// <summary> | |||
/// Provides a filter for which log levels to allow through. | |||
/// </summary> | |||
/// <value>the level to filter to</value> | |||
public abstract Logger.LogLevel Filter { get; set; } | |||
/// <summary> | |||
/// Prints a provided message from a given log at the specified time. | |||
/// </summary> | |||
/// <param name="level">the log level</param> | |||
/// <param name="time">the time the message was composed</param> | |||
/// <param name="logName">the name of the log that created this message</param> | |||
/// <param name="message">the message</param> | |||
public abstract void Print(Logger.Level level, DateTime time, string logName, string message); | |||
/// <summary> | |||
/// Called before the first print in a group. May be called multiple times. | |||
/// Use this to create file handles and the like. | |||
/// </summary> | |||
public virtual void StartPrint() { } | |||
/// <summary> | |||
/// Called after the last print in a group. May be called multiple times. | |||
/// Use this to dispose file handles and the like. | |||
/// </summary> | |||
public virtual void EndPrint() { } | |||
internal DateTime LastUse { get; set; } | |||
} | |||
#nullable enable | |||
using System; | |||
namespace IPA.Logging | |||
{ | |||
/// <summary> | |||
/// The log printer's base class. | |||
/// </summary> | |||
public abstract class LogPrinter | |||
{ | |||
/// <summary> | |||
/// Provides a filter for which log levels to allow through. | |||
/// </summary> | |||
/// <value>the level to filter to</value> | |||
public abstract Logger.LogLevel Filter { get; set; } | |||
/// <summary> | |||
/// Prints a provided message from a given log at the specified time. | |||
/// </summary> | |||
/// <param name="level">the log level</param> | |||
/// <param name="time">the time the message was composed</param> | |||
/// <param name="logName">the name of the log that created this message</param> | |||
/// <param name="message">the message</param> | |||
public abstract void Print(Logger.Level level, DateTime time, string logName, string message); | |||
/// <summary> | |||
/// Called before the first print in a group. May be called multiple times. | |||
/// Use this to create file handles and the like. | |||
/// </summary> | |||
public virtual void StartPrint() { } | |||
/// <summary> | |||
/// Called after the last print in a group. May be called multiple times. | |||
/// Use this to dispose file handles and the like. | |||
/// </summary> | |||
public virtual void EndPrint() { } | |||
internal DateTime LastUse { get; set; } | |||
} | |||
} |
@ -1,116 +1,159 @@ | |||
using System; | |||
using System.Runtime.InteropServices; | |||
namespace IPA.Logging.Printers | |||
{ | |||
/// <summary> | |||
/// Prints a pretty message to the console. | |||
/// </summary> | |||
public class ColoredConsolePrinter : LogPrinter | |||
{ | |||
private Logger.LogLevel filter = Logger.LogLevel.All; | |||
/// <summary> | |||
/// A filter for this specific printer. | |||
/// </summary> | |||
/// <value>the filter to apply to this printer</value> | |||
public override Logger.LogLevel Filter { get => filter; set => filter = value; } | |||
/// <summary> | |||
/// The color to print messages as. | |||
/// </summary> | |||
/// <value>the color to print this message as</value> | |||
// Initializer calls this function because Unity's .NET 3.5 doesn't have the color properties on Console | |||
public ConsoleColor Color { get; set; } = GetConsoleColor(WinConsole.OutHandle); | |||
/// <summary> | |||
/// Prints an entry to the console window. | |||
/// </summary> | |||
/// <param name="level">the <see cref="Logger.Level"/> of the message</param> | |||
/// <param name="time">the <see cref="DateTime"/> the message was recorded at</param> | |||
/// <param name="logName">the name of the log that sent the message</param> | |||
/// <param name="message">the message to print</param> | |||
public override void Print(Logger.Level level, DateTime time, string logName, string message) | |||
{ | |||
if (((byte)level & (byte)StandardLogger.PrintFilter) == 0) return; | |||
EnsureDefaultsPopulated(WinConsole.OutHandle); | |||
SetColor(Color, WinConsole.OutHandle); | |||
foreach (var line in message.Split(new[] { "\n", Environment.NewLine }, StringSplitOptions.RemoveEmptyEntries)) | |||
WinConsole.ConOut.WriteLine(Logger.LogFormat, line, logName, time, level.ToString().ToUpper()); | |||
ResetColor(WinConsole.OutHandle); | |||
} | |||
private static bool _haveReadDefaultColors; | |||
private static short _defaultColors; | |||
private void EnsureDefaultsPopulated(IntPtr handle, bool force = false) | |||
{ | |||
if (!_haveReadDefaultColors | force) | |||
{ | |||
GetConsoleScreenBufferInfo(handle, out var info); | |||
_defaultColors = (short)(info.Attribute & ~15); | |||
_haveReadDefaultColors = true; | |||
} | |||
} | |||
private void ResetColor(IntPtr handle) | |||
{ | |||
GetConsoleScreenBufferInfo(handle, out var info); | |||
var otherAttrs = (short)(info.Attribute & ~15); | |||
SetConsoleTextAttribute(handle, (short)(otherAttrs | _defaultColors)); | |||
} | |||
private void SetColor(ConsoleColor col, IntPtr handle) | |||
{ | |||
GetConsoleScreenBufferInfo(handle, out var info); | |||
var attr = GetAttrForeground(info.Attribute, col); | |||
SetConsoleTextAttribute(handle, attr); | |||
} | |||
private static short GetAttrForeground(int attr, ConsoleColor color) | |||
{ | |||
attr &= ~15; | |||
return (short)(attr | (int)color); | |||
} | |||
private static ConsoleColor GetConsoleColor(IntPtr handle) | |||
{ | |||
GetConsoleScreenBufferInfo(handle, out var info); | |||
return (ConsoleColor)(info.Attribute & 15); | |||
} | |||
// ReSharper disable NotAccessedField.Local | |||
#pragma warning disable 649 | |||
private struct Coordinate | |||
{ | |||
public short X; | |||
public short Y; | |||
} | |||
private struct SmallRect | |||
{ | |||
public short Left; | |||
public short Top; | |||
public short Right; | |||
public short Bottom; | |||
} | |||
private struct ConsoleScreenBufferInfo | |||
{ | |||
public Coordinate Size; | |||
public Coordinate CursorPosition; | |||
public short Attribute; | |||
public SmallRect Window; | |||
public Coordinate MaxWindowSize; | |||
} | |||
#pragma warning restore 649 | |||
// ReSharper restore NotAccessedField.Local | |||
[DllImport("kernel32.dll", EntryPoint = "GetConsoleScreenBufferInfo", SetLastError = true, CharSet = CharSet.Unicode)] | |||
private static extern bool GetConsoleScreenBufferInfo(IntPtr handle, out ConsoleScreenBufferInfo info); | |||
[DllImport("kernel32.dll", EntryPoint = "SetConsoleTextAttribute", SetLastError = true, CharSet = CharSet.Unicode)] | |||
private static extern bool SetConsoleTextAttribute(IntPtr handle, short attribute); | |||
} | |||
} | |||
#nullable enable | |||
using System; | |||
using System.Runtime.InteropServices; | |||
namespace IPA.Logging.Printers | |||
{ | |||
/// <summary> | |||
/// Prints a pretty message to the console. | |||
/// </summary> | |||
public class ColoredConsolePrinter : LogPrinter | |||
{ | |||
private Logger.LogLevel filter = Logger.LogLevel.All; | |||
/// <summary> | |||
/// A filter for this specific printer. | |||
/// </summary> | |||
/// <value>the filter to apply to this printer</value> | |||
public override Logger.LogLevel Filter { get => filter; set => filter = value; } | |||
/// <summary> | |||
/// The color to print messages as. | |||
/// </summary> | |||
/// <value>the color to print this message as</value> | |||
// Initializer calls this function because Unity's .NET 3.5 doesn't have the color properties on Console | |||
public ConsoleColor Color { get; set; } = GetConsoleColor(WinConsole.OutHandle); | |||
private static ConsoleColor GetDarkenedColor(ConsoleColor color) | |||
=> color switch | |||
{ | |||
ConsoleColor.Gray => ConsoleColor.DarkGray, | |||
ConsoleColor.Blue => ConsoleColor.DarkBlue, | |||
ConsoleColor.Green => ConsoleColor.DarkGreen, | |||
ConsoleColor.Cyan => ConsoleColor.DarkCyan, | |||
ConsoleColor.Red => ConsoleColor.DarkRed, | |||
ConsoleColor.Magenta => ConsoleColor.DarkMagenta, | |||
ConsoleColor.Yellow => ConsoleColor.DarkYellow, | |||
ConsoleColor.White => ConsoleColor.Gray, | |||
_ => color, | |||
}; | |||
private readonly bool darkenSetManually; | |||
private readonly bool darkenMessages; | |||
public ColoredConsolePrinter() : this(Config.SelfConfig.Debug_.DarkenMessages_) | |||
{ | |||
darkenSetManually = false; | |||
} | |||
public ColoredConsolePrinter(bool darkenMessages) | |||
{ | |||
darkenSetManually = true; | |||
this.darkenMessages = darkenMessages; | |||
} | |||
/// <summary> | |||
/// Prints an entry to the console window. | |||
/// </summary> | |||
/// <param name="level">the <see cref="Logger.Level"/> of the message</param> | |||
/// <param name="time">the <see cref="DateTime"/> the message was recorded at</param> | |||
/// <param name="logName">the name of the log that sent the message</param> | |||
/// <param name="message">the message to print</param> | |||
public override void Print(Logger.Level level, DateTime time, string logName, string message) | |||
{ | |||
if (((byte)level & (byte)StandardLogger.PrintFilter) == 0) return; | |||
EnsureDefaultsPopulated(WinConsole.OutHandle); | |||
SetColor(Color, WinConsole.OutHandle); | |||
var prefixStr = ""; | |||
var suffixStr = ""; | |||
if ((darkenSetManually && darkenMessages) || (!darkenSetManually && Config.SelfConfig.Debug_.DarkenMessages_)) | |||
{ | |||
var darkened = GetDarkenedColor(Color); | |||
if (darkened != Color) | |||
{ | |||
prefixStr = StdoutInterceptor.ConsoleColorToForegroundSet(darkened); | |||
suffixStr = StdoutInterceptor.ConsoleColorToForegroundSet(Color); | |||
} | |||
} | |||
foreach (var line in message.Split(new[] { "\n", Environment.NewLine }, StringSplitOptions.RemoveEmptyEntries)) | |||
WinConsole.ConOut.WriteLine(Logger.LogFormat, prefixStr + line + suffixStr, logName, time, level.ToString().ToUpperInvariant()); | |||
ResetColor(WinConsole.OutHandle); | |||
} | |||
private static bool _haveReadDefaultColors; | |||
private static short _defaultColors; | |||
private static void EnsureDefaultsPopulated(IntPtr handle, bool force = false) | |||
{ | |||
if (!_haveReadDefaultColors | force) | |||
{ | |||
_ = GetConsoleScreenBufferInfo(handle, out var info); | |||
_defaultColors = (short)(info.Attribute & ~15); | |||
_haveReadDefaultColors = true; | |||
} | |||
} | |||
private static void ResetColor(IntPtr handle) | |||
{ | |||
_ = GetConsoleScreenBufferInfo(handle, out var info); | |||
var otherAttrs = (short)(info.Attribute & ~15); | |||
_ = SetConsoleTextAttribute(handle, (short)(otherAttrs | _defaultColors)); | |||
} | |||
private static void SetColor(ConsoleColor col, IntPtr handle) | |||
{ | |||
_ = GetConsoleScreenBufferInfo(handle, out var info); | |||
var attr = GetAttrForeground(info.Attribute, col); | |||
_ = SetConsoleTextAttribute(handle, attr); | |||
} | |||
private static short GetAttrForeground(int attr, ConsoleColor color) | |||
{ | |||
attr &= ~15; | |||
return (short)(attr | (int)color); | |||
} | |||
private static ConsoleColor GetConsoleColor(IntPtr handle) | |||
{ | |||
_ = GetConsoleScreenBufferInfo(handle, out var info); | |||
return (ConsoleColor)(info.Attribute & 15); | |||
} | |||
// ReSharper disable NotAccessedField.Local | |||
#pragma warning disable 649 | |||
private struct Coordinate | |||
{ | |||
public short X; | |||
public short Y; | |||
} | |||
private struct SmallRect | |||
{ | |||
public short Left; | |||
public short Top; | |||
public short Right; | |||
public short Bottom; | |||
} | |||
private struct ConsoleScreenBufferInfo | |||
{ | |||
public Coordinate Size; | |||
public Coordinate CursorPosition; | |||
public short Attribute; | |||
public SmallRect Window; | |||
public Coordinate MaxWindowSize; | |||
} | |||
#pragma warning restore 649 | |||
// ReSharper restore NotAccessedField.Local | |||
[DllImport("kernel32.dll", EntryPoint = "GetConsoleScreenBufferInfo", SetLastError = true, CharSet = CharSet.Unicode)] | |||
private static extern bool GetConsoleScreenBufferInfo(IntPtr handle, out ConsoleScreenBufferInfo info); | |||
[DllImport("kernel32.dll", EntryPoint = "SetConsoleTextAttribute", SetLastError = true, CharSet = CharSet.Unicode)] | |||
private static extern bool SetConsoleTextAttribute(IntPtr handle, short attribute); | |||
} | |||
} |
@ -1,158 +1,170 @@ | |||
using Ionic.Zlib; | |||
using System; | |||
using System.IO; | |||
using System.Runtime.InteropServices; | |||
using System.Text; | |||
using System.Text.RegularExpressions; | |||
#if NET3 | |||
using Net3_Proxy; | |||
using Path = Net3_Proxy.Path; | |||
#endif | |||
namespace IPA.Logging.Printers | |||
{ | |||
/// <summary> | |||
/// A <see cref="LogPrinter"/> abstract class that provides the utilities to write to a GZip file. | |||
/// </summary> | |||
public abstract class GZFilePrinter : LogPrinter, IDisposable | |||
{ | |||
[DllImport("Kernel32.dll", CharSet = CharSet.Unicode, SetLastError = true)] | |||
private static extern bool CreateHardLink( | |||
string lpFileName, | |||
string lpExistingFileName, | |||
IntPtr lpSecurityAttributes | |||
); | |||
#if NET4 | |||
private const RegexOptions reOptions = RegexOptions.Compiled; | |||
#elif NET3 // Needed because Compiled doesn't exist in Unity's .NET 3 runtime | |||
private const RegexOptions reOptions = RegexOptions.None; | |||
#endif | |||
internal static Regex removeControlCodes = new Regex("\x1b\\[\\d+m", reOptions); | |||
private FileInfo fileInfo; | |||
/// <summary> | |||
/// The <see cref="StreamWriter"/> that writes to the GZip file. | |||
/// </summary> | |||
/// <value>the writer to the underlying filestream</value> | |||
protected StreamWriter FileWriter; | |||
private FileStream fstream; | |||
/// <summary> | |||
/// Gets the <see cref="FileInfo"/> for the file to write to. | |||
/// </summary> | |||
/// <returns>the file to write to</returns> | |||
protected abstract FileInfo GetFileInfo(); | |||
private const string latestFormat = "_latest{0}"; | |||
private void InitLog() | |||
{ | |||
try | |||
{ | |||
if (fileInfo == null) | |||
{ // first init | |||
fileInfo = GetFileInfo(); | |||
var ext = fileInfo.Extension; | |||
var symlink = new FileInfo(Path.Combine(fileInfo.DirectoryName ?? throw new InvalidOperationException(), string.Format(latestFormat, ext))); | |||
if (symlink.Exists) symlink.Delete(); | |||
foreach (var file in fileInfo.Directory.EnumerateFiles("*.log", SearchOption.TopDirectoryOnly)) | |||
{ | |||
if (file.Equals(fileInfo)) continue; | |||
if (file.Extension == ".gz") continue; | |||
CompressOldLog(file); | |||
} | |||
fileInfo.Create().Close(); | |||
try | |||
{ | |||
if (!CreateHardLink(symlink.FullName, fileInfo.FullName, IntPtr.Zero)) | |||
{ | |||
var error = Marshal.GetLastWin32Error(); | |||
Logger.log.Error($"Hardlink creation failed ({error})"); | |||
} | |||
} | |||
catch (Exception e) | |||
{ | |||
Logger.log.Error("Error creating latest hardlink!"); | |||
Logger.log.Error(e); | |||
} | |||
} | |||
} | |||
catch (Exception e) | |||
{ | |||
Logger.log.Error("Error initializing log!"); | |||
Logger.log.Error(e); | |||
} | |||
} | |||
private static async void CompressOldLog(FileInfo file) | |||
{ | |||
Logger.log.Debug($"Compressing log file {file}"); | |||
var newFile = new FileInfo(file.FullName + ".gz"); | |||
using (var istream = file.OpenRead()) | |||
using (var ostream = newFile.Create()) | |||
using (var gz = new GZipStream(ostream, CompressionMode.Compress, CompressionLevel.BestCompression, false)) | |||
await istream.CopyToAsync(gz); | |||
file.Delete(); | |||
} | |||
/// <summary> | |||
/// Called at the start of any print session. | |||
/// </summary> | |||
public sealed override void StartPrint() | |||
{ | |||
InitLog(); | |||
fstream = fileInfo.Open(FileMode.Append, FileAccess.Write); | |||
FileWriter = new StreamWriter(fstream, new UTF8Encoding(false)); | |||
} | |||
/// <summary> | |||
/// Called at the end of any print session. | |||
/// </summary> | |||
public sealed override void EndPrint() | |||
{ | |||
FileWriter.Flush(); | |||
fstream.Flush(); | |||
FileWriter.Dispose(); | |||
fstream.Dispose(); | |||
FileWriter = null; | |||
fstream = null; | |||
} | |||
/// <inheritdoc /> | |||
public void Dispose() | |||
{ | |||
Dispose(true); | |||
GC.SuppressFinalize(this); | |||
} | |||
/// <summary> | |||
/// Disposes the file printer. | |||
/// </summary> | |||
/// <param name="disposing">does nothing</param> | |||
protected virtual void Dispose(bool disposing) | |||
{ | |||
if (disposing) | |||
{ | |||
FileWriter.Flush(); | |||
fstream.Flush(); | |||
FileWriter.Close(); | |||
fstream.Close(); | |||
FileWriter.Dispose(); | |||
fstream.Dispose(); | |||
} | |||
} | |||
} | |||
#nullable enable | |||
using Ionic.Zlib; | |||
using System; | |||
using System.Diagnostics.CodeAnalysis; | |||
using System.IO; | |||
using System.Runtime.InteropServices; | |||
using System.Text; | |||
using System.Text.RegularExpressions; | |||
#if NET3 | |||
using Net3_Proxy; | |||
using Path = Net3_Proxy.Path; | |||
#endif | |||
namespace IPA.Logging.Printers | |||
{ | |||
/// <summary> | |||
/// A <see cref="LogPrinter"/> abstract class that provides the utilities to write to a GZip file. | |||
/// </summary> | |||
public abstract class GZFilePrinter : LogPrinter, IDisposable | |||
{ | |||
[DllImport("Kernel32.dll", CharSet = CharSet.Unicode, SetLastError = true)] | |||
private static extern bool CreateHardLink( | |||
string lpFileName, | |||
string lpExistingFileName, | |||
IntPtr lpSecurityAttributes | |||
); | |||
#if NET4 | |||
private const RegexOptions reOptions = RegexOptions.Compiled; | |||
#elif NET3 // Needed because Compiled doesn't exist in Unity's .NET 3 runtime | |||
private const RegexOptions reOptions = RegexOptions.None; | |||
#endif | |||
internal static Regex removeControlCodes = new("\x1b\\[\\d+m", reOptions); | |||
private FileInfo? fileInfo; | |||
/// <summary> | |||
/// The <see cref="StreamWriter"/> that writes to the GZip file. | |||
/// </summary> | |||
/// <value>the writer to the underlying filestream</value> | |||
protected StreamWriter? FileWriter; | |||
private FileStream? fstream; | |||
/// <summary> | |||
/// Gets the <see cref="FileInfo"/> for the file to write to. | |||
/// </summary> | |||
/// <returns>the file to write to</returns> | |||
protected abstract FileInfo GetFileInfo(); | |||
private const string latestFormat = "_latest{0}"; | |||
[MemberNotNull(nameof(fileInfo))] | |||
private void InitLog() | |||
{ | |||
try | |||
{ | |||
if (fileInfo == null) | |||
{ // first init | |||
fileInfo = GetFileInfo(); | |||
var ext = fileInfo.Extension; | |||
var symlink = new FileInfo(Path.Combine(fileInfo.DirectoryName ?? throw new InvalidOperationException(), string.Format(latestFormat, ext))); | |||
if (symlink.Exists) symlink.Delete(); | |||
foreach (var file in fileInfo.Directory.EnumerateFiles("*.log", SearchOption.TopDirectoryOnly)) | |||
{ | |||
if (file.Equals(fileInfo)) continue; | |||
if (file.Extension == ".gz") continue; | |||
CompressOldLog(file); | |||
} | |||
fileInfo.Create().Close(); | |||
try | |||
{ | |||
if (!CreateHardLink(symlink.FullName, fileInfo.FullName, IntPtr.Zero)) | |||
{ | |||
var error = Marshal.GetLastWin32Error(); | |||
Logger.Default.Error($"Hardlink creation failed ({error})"); | |||
} | |||
} | |||
catch (Exception e) | |||
{ | |||
Logger.Default.Error("Error creating latest hardlink!"); | |||
Logger.Default.Error(e); | |||
} | |||
} | |||
} | |||
catch (Exception e) | |||
{ | |||
Logger.Default.Error("Error initializing log!"); | |||
Logger.Default.Error(e); | |||
throw; | |||
} | |||
} | |||
private static async void CompressOldLog(FileInfo file) | |||
{ | |||
try | |||
{ | |||
Logger.Default.Debug($"Compressing log file {file}"); | |||
var newFile = new FileInfo(file.FullName + ".gz"); | |||
using (var istream = file.OpenRead()) | |||
using (var ostream = newFile.Create()) | |||
using (var gz = new GZipStream(ostream, CompressionMode.Compress, CompressionLevel.BestCompression, false)) | |||
await istream.CopyToAsync(gz).ConfigureAwait(false); | |||
file.Delete(); | |||
} | |||
catch (Exception e) | |||
{ | |||
Logger.Default.Error("Error compressing old log file:"); | |||
Logger.Default.Error(e); | |||
} | |||
} | |||
/// <summary> | |||
/// Called at the start of any print session. | |||
/// </summary> | |||
public sealed override void StartPrint() | |||
{ | |||
InitLog(); | |||
fstream = fileInfo.Open(FileMode.Append, FileAccess.Write); | |||
FileWriter = new StreamWriter(fstream, new UTF8Encoding(false)); | |||
} | |||
/// <summary> | |||
/// Called at the end of any print session. | |||
/// </summary> | |||
public sealed override void EndPrint() | |||
{ | |||
FileWriter?.Flush(); | |||
fstream?.Flush(); | |||
FileWriter?.Dispose(); | |||
fstream?.Dispose(); | |||
FileWriter = null; | |||
fstream = null; | |||
} | |||
/// <inheritdoc /> | |||
public void Dispose() | |||
{ | |||
Dispose(true); | |||
GC.SuppressFinalize(this); | |||
} | |||
/// <summary> | |||
/// Disposes the file printer. | |||
/// </summary> | |||
/// <param name="disposing">does nothing</param> | |||
protected virtual void Dispose(bool disposing) | |||
{ | |||
if (disposing) | |||
{ | |||
FileWriter?.Flush(); | |||
fstream?.Flush(); | |||
FileWriter?.Close(); | |||
fstream?.Close(); | |||
FileWriter?.Dispose(); | |||
fstream?.Dispose(); | |||
} | |||
} | |||
} | |||
} |
@ -1,270 +1,323 @@ | |||
using IPA.Config.Data; | |||
using IPA.Config.Stores; | |||
using IPA.Config.Stores.Converters; | |||
using System; | |||
using System.Collections.Generic; | |||
using Version = SemVer.Version; | |||
namespace IPA.Utilities | |||
{ | |||
/// <summary> | |||
/// A type that wraps <see cref="Version"/> so that the string of the version is stored when the string is | |||
/// not a valid <see cref="Version"/>. | |||
/// </summary> | |||
public class AlmostVersion : IComparable<AlmostVersion>, IComparable<Version> | |||
{ | |||
/// <summary> | |||
/// Represents a storage type of either parsed <see cref="Version"/> object or raw <see cref="String"/>. | |||
/// </summary> | |||
public enum StoredAs | |||
{ | |||
/// <summary> | |||
/// The version was stored as a <see cref="Version"/>. | |||
/// </summary> | |||
SemVer, | |||
/// <summary> | |||
/// The version was stored as a <see cref="String"/>. | |||
/// </summary> | |||
String | |||
} | |||
/// <summary> | |||
/// Creates a new <see cref="AlmostVersion"/> with the version string provided in <paramref name="vertext"/>. | |||
/// </summary> | |||
/// <param name="vertext">the version string to store</param> | |||
public AlmostVersion(string vertext) | |||
{ | |||
if (!TryParseFrom(vertext, StoredAs.SemVer)) | |||
TryParseFrom(vertext, StoredAs.String); | |||
} | |||
/// <summary> | |||
/// Creates an <see cref="AlmostVersion"/> from the <see cref="Version"/> provided in <paramref name="ver"/>. | |||
/// </summary> | |||
/// <param name="ver">the <see cref="Version"/> to store</param> | |||
public AlmostVersion(Version ver) | |||
{ | |||
SemverValue = ver; | |||
StorageMode = StoredAs.SemVer; | |||
} | |||
/// <summary> | |||
/// Creates an <see cref="AlmostVersion"/> from the version string in <paramref name="vertext"/> stored using | |||
/// the storage mode specified in <paramref name="mode"/>. | |||
/// </summary> | |||
/// <param name="vertext">the text to parse as an <see cref="AlmostVersion"/></param> | |||
/// <param name="mode">the storage mode to store the version in</param> | |||
public AlmostVersion(string vertext, StoredAs mode) | |||
{ | |||
if (!TryParseFrom(vertext, mode)) | |||
throw new ArgumentException($"{nameof(vertext)} could not be stored as {mode}!"); | |||
} | |||
/// <summary> | |||
/// Creates a new <see cref="AlmostVersion"/> from the version string in <paramref name="vertext"/> stored the | |||
/// same way as the <see cref="AlmostVersion"/> passed in <paramref name="copyMode"/>. | |||
/// </summary> | |||
/// <param name="vertext">the text to parse as an <see cref="AlmostVersion"/></param> | |||
/// <param name="copyMode">an <see cref="AlmostVersion"/> to copy the storage mode of</param> | |||
public AlmostVersion(string vertext, AlmostVersion copyMode) | |||
{ | |||
if (copyMode == null) | |||
throw new ArgumentNullException(nameof(copyMode)); | |||
if (!TryParseFrom(vertext, copyMode.StorageMode)) | |||
TryParseFrom(vertext, StoredAs.String); // silently parse differently | |||
} | |||
private bool TryParseFrom(string str, StoredAs mode) | |||
{ | |||
if (mode == StoredAs.SemVer) | |||
try | |||
{ | |||
SemverValue = new Version(str, true); | |||
StorageMode = StoredAs.SemVer; | |||
return true; | |||
} | |||
catch | |||
{ | |||
return false; | |||
} | |||
else | |||
{ | |||
StringValue = str; | |||
StorageMode = StoredAs.String; | |||
return true; | |||
} | |||
} | |||
/// <summary> | |||
/// The value of the <see cref="AlmostVersion"/> if it was stored as a <see cref="string"/>. | |||
/// </summary> | |||
/// <value>the stored value as a <see cref="string"/>, or <see langword="null"/> if not stored as a string.</value> | |||
public string StringValue { get; private set; } = null; | |||
/// <summary> | |||
/// The value of the <see cref="AlmostVersion"/> if it was stored as a <see cref="Version"/>. | |||
/// </summary> | |||
/// <value>the stored value as a <see cref="Version"/>, or <see langword="null"/> if not stored as a version.</value> | |||
public Version SemverValue { get; private set; } = null; | |||
/// <summary> | |||
/// The way the value is stored, whether it be as a <see cref="Version"/> or a <see cref="string"/>. | |||
/// </summary> | |||
/// <value>the storage mode used to store this value</value> | |||
public StoredAs StorageMode { get; private set; } | |||
// can I just <inheritdoc /> this? | |||
/// <summary> | |||
/// Gets a string representation of the current version. If the value is stored as a string, this returns it. If it is | |||
/// stored as a <see cref="Version"/>, it is equivalent to calling <see cref="Version.ToString"/>. | |||
/// </summary> | |||
/// <returns>a string representation of the current version</returns> | |||
/// <seealso cref="object.ToString"/> | |||
public override string ToString() => | |||
StorageMode == StoredAs.SemVer ? SemverValue.ToString() : StringValue; | |||
/// <summary> | |||
/// Compares <see langword="this"/> to the <see cref="AlmostVersion"/> in <paramref name="other"/> using <see cref="Version.CompareTo(Version)"/> | |||
/// or <see cref="string.CompareTo(string)"/>, depending on the current store. | |||
/// </summary> | |||
/// <remarks> | |||
/// The storage methods of the two objects must be the same, or this will throw an <see cref="InvalidOperationException"/>. | |||
/// </remarks> | |||
/// <param name="other">the <see cref="AlmostVersion"/> to compare to</param> | |||
/// <returns>less than 0 if <paramref name="other"/> is considered bigger than <see langword="this"/>, 0 if equal, and greater than zero if smaller</returns> | |||
/// <seealso cref="CompareTo(Version)"/> | |||
public int CompareTo(AlmostVersion other) | |||
{ | |||
if (other == null) return -1; | |||
if (StorageMode != other.StorageMode) | |||
throw new InvalidOperationException("Cannot compare AlmostVersions with different stores!"); | |||
if (StorageMode == StoredAs.SemVer) | |||
return SemverValue.CompareTo(other.SemverValue); | |||
else | |||
return StringValue.CompareTo(other.StringValue); | |||
} | |||
/// <summary> | |||
/// Compares <see langword="this"/> to the <see cref="Version"/> in <paramref name="other"/> using <see cref="Version.CompareTo(Version)"/>. | |||
/// </summary> | |||
/// <remarks> | |||
/// The storage method of <see langword="this"/> must be <see cref="StoredAs.SemVer"/>, else an <see cref="InvalidOperationException"/> will | |||
/// be thrown. | |||
/// </remarks> | |||
/// <param name="other">the <see cref="Version"/> to compare to</param> | |||
/// <returns>less than 0 if <paramref name="other"/> is considered bigger than <see langword="this"/>, 0 if equal, and greater than zero if smaller</returns> | |||
/// <seealso cref="CompareTo(AlmostVersion)"/> | |||
public int CompareTo(Version other) | |||
{ | |||
if (StorageMode != StoredAs.SemVer) | |||
throw new InvalidOperationException("Cannot compare a SemVer version with an AlmostVersion stored as a string!"); | |||
return SemverValue.CompareTo(other); | |||
} | |||
/// <summary> | |||
/// Performs a strict equality check between <see langword="this"/> and <paramref name="obj"/>. | |||
/// </summary> | |||
/// <remarks> | |||
/// This may return <see langword="false"/> where <see cref="operator ==(AlmostVersion, AlmostVersion)"/> returns <see langword="true"/> | |||
/// </remarks> | |||
/// <param name="obj">the object to compare to</param> | |||
/// <returns><see langword="true"/> if they are equal, <see langword="false"/> otherwise</returns> | |||
/// <seealso cref="object.Equals(object)"/> | |||
public override bool Equals(object obj) | |||
{ | |||
return obj is AlmostVersion version && | |||
SemverValue == version.SemverValue && | |||
StringValue == version.StringValue && | |||
StorageMode == version.StorageMode; | |||
} | |||
/// <summary> | |||
/// Default generated hash code function generated by VS. | |||
/// </summary> | |||
/// <returns>a value unique to each object, except those that are considered equal by <see cref="Equals(object)"/></returns> | |||
/// <seealso cref="object.GetHashCode"/> | |||
public override int GetHashCode() | |||
{ | |||
var hashCode = -126402897; | |||
hashCode = hashCode * -1521134295 + EqualityComparer<Version>.Default.GetHashCode(SemverValue); | |||
hashCode = hashCode * -1521134295 + EqualityComparer<string>.Default.GetHashCode(StringValue); | |||
hashCode = hashCode * -1521134295 + StorageMode.GetHashCode(); | |||
return hashCode; | |||
} | |||
/// <summary> | |||
/// Compares two versions, only taking into account the numeric part of the version if they are stored as <see cref="Version"/>s, | |||
/// or strict equality if they are stored as <see cref="string"/>s. | |||
/// </summary> | |||
/// <remarks> | |||
/// This is a looser equality than <see cref="Equals(object)"/>, meaning that this may return <see langword="true"/> where <see cref="Equals(object)"/> | |||
/// does not. | |||
/// </remarks> | |||
/// <param name="l">the first value to compare</param> | |||
/// <param name="r">the second value to compare</param> | |||
/// <returns><see langword="true"/> if they are mostly equal, <see langword="false"/> otherwise</returns> | |||
/// <seealso cref="Equals(object)"/> | |||
public static bool operator==(AlmostVersion l, AlmostVersion r) | |||
{ | |||
if (l is null && r is null) return true; | |||
if (l is null || r is null) return false; | |||
if (l.StorageMode != r.StorageMode) return false; | |||
if (l.StorageMode == StoredAs.SemVer) | |||
return Utils.VersionCompareNoPrerelease(l.SemverValue, r.SemverValue) == 0; | |||
else | |||
return l.StringValue == r.StringValue; | |||
} | |||
/// <summary> | |||
/// The opposite of <see cref="operator ==(AlmostVersion, AlmostVersion)"/>. Equivalent to <c>!(l == r)</c>. | |||
/// </summary> | |||
/// <param name="l">the first value to compare</param> | |||
/// <param name="r">the second value to compare</param> | |||
/// <returns><see langword="true"/> if they are not mostly equal, <see langword="false"/> otherwise</returns> | |||
/// <seealso cref="operator ==(AlmostVersion, AlmostVersion)"/> | |||
public static bool operator!=(AlmostVersion l, AlmostVersion r) => !(l == r); | |||
// implicitly convertible from Version | |||
/// <summary> | |||
/// Implicitly converts a <see cref="Version"/> to <see cref="AlmostVersion"/> using <see cref="AlmostVersion(Version)"/>. | |||
/// </summary> | |||
/// <param name="ver">the <see cref="Version"/> to convert</param> | |||
/// <seealso cref="AlmostVersion(Version)"/> | |||
public static implicit operator AlmostVersion(Version ver) => new AlmostVersion(ver); | |||
// implicitly convertible to Version | |||
/// <summary> | |||
/// Implicitly converts an <see cref="AlmostVersion"/> to <see cref="Version"/>, if applicable, using <see cref="SemverValue"/>. | |||
/// If not applicable, returns <see langword="null"/> | |||
/// </summary> | |||
/// <param name="av">the <see cref="AlmostVersion"/> to convert to a <see cref="Version"/></param> | |||
/// <seealso cref="SemverValue"/> | |||
public static implicit operator Version(AlmostVersion av) => av?.SemverValue; | |||
} | |||
/// <summary> | |||
/// A <see cref="ValueConverter{T}"/> for <see cref="AlmostVersion"/>s. | |||
/// </summary> | |||
public sealed class AlmostVersionConverter : ValueConverter<AlmostVersion> | |||
{ | |||
/// <summary> | |||
/// Converts a <see cref="Text"/> node into an <see cref="AlmostVersion"/>. | |||
/// </summary> | |||
/// <param name="value">the <see cref="Text"/> node to convert</param> | |||
/// <param name="parent">the owner of the new object</param> | |||
/// <returns></returns> | |||
public override AlmostVersion FromValue(Value value, object parent) | |||
=> new AlmostVersion(Converter<string>.Default.FromValue(value, parent)); | |||
/// <summary> | |||
/// Converts an <see cref="AlmostVersion"/> to a <see cref="Text"/> node. | |||
/// </summary> | |||
/// <param name="obj">the <see cref="AlmostVersion"/> to convert</param> | |||
/// <param name="parent">the parent of <paramref name="obj"/></param> | |||
/// <returns>a <see cref="Text"/> node representing <paramref name="obj"/></returns> | |||
public override Value ToValue(AlmostVersion obj, object parent) | |||
=> Value.From(obj.ToString()); | |||
} | |||
} | |||
#nullable enable | |||
using IPA.Config.Data; | |||
using IPA.Config.Stores; | |||
using IPA.Config.Stores.Converters; | |||
using System; | |||
using System.Collections.Generic; | |||
using SVersion = SemVer.Version; | |||
using Version = Hive.Versioning.Version; | |||
namespace IPA.Utilities | |||
{ | |||
/// <summary> | |||
/// A type that wraps <see cref="Version"/> so that the string of the version is stored when the string is | |||
/// not a valid <see cref="Version"/>. | |||
/// </summary> | |||
public class AlmostVersion : IComparable<AlmostVersion>, IComparable<Version>, | |||
#pragma warning disable CS0618 // Type or member is obsolete | |||
IComparable<SVersion> | |||
#pragma warning restore CS0618 // Type or member is obsolete | |||
{ | |||
/// <summary> | |||
/// Represents a storage type of either parsed <see cref="Version"/> object or raw <see cref="String"/>. | |||
/// </summary> | |||
public enum StoredAs | |||
{ | |||
/// <summary> | |||
/// The version was stored as a <see cref="SVersion"/>. | |||
/// </summary> | |||
SemVer, | |||
/// <summary> | |||
/// The version was stored as a <see cref="String"/>. | |||
/// </summary> | |||
String | |||
} | |||
/// <summary> | |||
/// Creates a new <see cref="AlmostVersion"/> with the version string provided in <paramref name="vertext"/>. | |||
/// </summary> | |||
/// <param name="vertext">the version string to store</param> | |||
public AlmostVersion(string vertext) | |||
{ | |||
if (!TryParseFrom(vertext, StoredAs.SemVer)) | |||
_ = TryParseFrom(vertext, StoredAs.String); | |||
} | |||
/// <summary> | |||
/// Creates an <see cref="AlmostVersion"/> from the <see cref="Version"/> provided in <paramref name="ver"/>. | |||
/// </summary> | |||
/// <param name="ver">the <see cref="Version"/> to store</param> | |||
public AlmostVersion(Version ver) | |||
{ | |||
SemverValue = ver; | |||
StorageMode = StoredAs.SemVer; | |||
} | |||
/// <summary> | |||
/// Creates an <see cref="AlmostVersion"/> from the <see cref="SVersion"/> provided in <paramref name="ver"/>. | |||
/// </summary> | |||
/// <param name="ver">the <see cref="SVersion"/> to store</param> | |||
[Obsolete("Use Hive.Versioning.Version constructor instead.")] | |||
public AlmostVersion(SVersion ver) : this(ver?.UnderlyingVersion ?? throw new ArgumentNullException(nameof(ver))) { } | |||
/// <summary> | |||
/// Creates an <see cref="AlmostVersion"/> from the version string in <paramref name="vertext"/> stored using | |||
/// the storage mode specified in <paramref name="mode"/>. | |||
/// </summary> | |||
/// <param name="vertext">the text to parse as an <see cref="AlmostVersion"/></param> | |||
/// <param name="mode">the storage mode to store the version in</param> | |||
public AlmostVersion(string vertext, StoredAs mode) | |||
{ | |||
if (!TryParseFrom(vertext, mode)) | |||
throw new ArgumentException($"{nameof(vertext)} could not be stored as {mode}!"); | |||
} | |||
/// <summary> | |||
/// Creates a new <see cref="AlmostVersion"/> from the version string in <paramref name="vertext"/> stored the | |||
/// same way as the <see cref="AlmostVersion"/> passed in <paramref name="copyMode"/>. | |||
/// </summary> | |||
/// <param name="vertext">the text to parse as an <see cref="AlmostVersion"/></param> | |||
/// <param name="copyMode">an <see cref="AlmostVersion"/> to copy the storage mode of</param> | |||
public AlmostVersion(string vertext, AlmostVersion copyMode) | |||
{ | |||
if (copyMode is null) | |||
throw new ArgumentNullException(nameof(copyMode)); | |||
if (!TryParseFrom(vertext, copyMode.StorageMode)) | |||
_ = TryParseFrom(vertext, StoredAs.String); // silently parse differently | |||
} | |||
private bool TryParseFrom(string str, StoredAs mode) | |||
{ | |||
if (mode == StoredAs.SemVer) | |||
{ | |||
StorageMode = StoredAs.SemVer; | |||
var result = Version.TryParse(str, out var version); | |||
SemverValue = version; | |||
return result; | |||
} | |||
else | |||
{ | |||
StringValue = str; | |||
StorageMode = StoredAs.String; | |||
return true; | |||
} | |||
} | |||
/// <summary> | |||
/// The value of the <see cref="AlmostVersion"/> if it was stored as a <see cref="string"/>. | |||
/// </summary> | |||
/// <value>the stored value as a <see cref="string"/>, or <see langword="null"/> if not stored as a string.</value> | |||
public string? StringValue { get; private set; } | |||
/// <summary> | |||
/// The value of the <see cref="AlmostVersion"/> if it was stored as a <see cref="Version"/>. | |||
/// </summary> | |||
/// <value>the stored value as a <see cref="Version"/>, or <see langword="null"/> if not stored as a version.</value> | |||
public Version? SemverValue { get; private set; } | |||
/// <summary> | |||
/// The way the value is stored, whether it be as a <see cref="Version"/> or a <see cref="string"/>. | |||
/// </summary> | |||
/// <value>the storage mode used to store this value</value> | |||
public StoredAs StorageMode { get; private set; } | |||
/// <summary> | |||
/// Gets a string representation of the current version. If the value is stored as a string, this returns it. If it is | |||
/// stored as a <see cref="Version"/>, it is equivalent to calling <see cref="Version.ToString()"/>. | |||
/// </summary> | |||
/// <returns>a string representation of the current version</returns> | |||
/// <seealso cref="object.ToString"/> | |||
public override string ToString() => | |||
StorageMode == StoredAs.SemVer ? SemverValue!.ToString() : StringValue!; | |||
/// <summary> | |||
/// Compares <see langword="this"/> to the <see cref="AlmostVersion"/> in <paramref name="other"/> using <see cref="Version.CompareTo(Version)"/> | |||
/// or <see cref="string.CompareTo(string)"/>, depending on the current store. | |||
/// </summary> | |||
/// <remarks> | |||
/// The storage methods of the two objects must be the same, or this will throw an <see cref="InvalidOperationException"/>. | |||
/// </remarks> | |||
/// <param name="other">the <see cref="AlmostVersion"/> to compare to</param> | |||
/// <returns>less than 0 if <paramref name="other"/> is considered bigger than <see langword="this"/>, 0 if equal, and greater than zero if smaller</returns> | |||
/// <seealso cref="CompareTo(Version)"/> | |||
public int CompareTo(AlmostVersion other) | |||
{ | |||
if (other is null) return 1; | |||
return StorageMode == StoredAs.SemVer && other.StorageMode == StoredAs.SemVer | |||
? SemverValue!.CompareTo(other.SemverValue!) | |||
: string.Compare(ToString(), other.ToString(), StringComparison.Ordinal); | |||
} | |||
/// <summary> | |||
/// Compares <see langword="this"/> to the <see cref="Version"/> in <paramref name="other"/> using <see cref="Version.CompareTo(Version)"/>. | |||
/// </summary> | |||
/// <remarks> | |||
/// The storage method of <see langword="this"/> must be <see cref="StoredAs.SemVer"/>, else an <see cref="InvalidOperationException"/> will | |||
/// be thrown. | |||
/// </remarks> | |||
/// <param name="other">the <see cref="Version"/> to compare to</param> | |||
/// <returns>less than 0 if <paramref name="other"/> is considered bigger than <see langword="this"/>, 0 if equal, and greater than zero if smaller</returns> | |||
/// <seealso cref="CompareTo(AlmostVersion)"/> | |||
public int CompareTo(Version other) | |||
{ | |||
if (StorageMode != StoredAs.SemVer) | |||
throw new InvalidOperationException("Cannot compare a SemVer version with an AlmostVersion stored as a string!"); | |||
return SemverValue!.CompareTo(other); | |||
} | |||
/// <summary> | |||
/// Compares <see langword="this"/> to the <see cref="SVersion"/> in <paramref name="other"/> using <see cref="Version.CompareTo(Version)"/>. | |||
/// </summary> | |||
/// <remarks> | |||
/// The storage method of <see langword="this"/> must be <see cref="StoredAs.SemVer"/>, else an <see cref="InvalidOperationException"/> will | |||
/// be thrown. | |||
/// </remarks> | |||
/// <param name="other">the <see cref="SVersion"/> to compare to</param> | |||
/// <returns>less than 0 if <paramref name="other"/> is considered bigger than <see langword="this"/>, 0 if equal, and greater than zero if smaller</returns> | |||
/// <seealso cref="CompareTo(AlmostVersion)"/> | |||
[Obsolete("Use the Hive.Versioning.Version overload instead.")] | |||
public int CompareTo(SVersion other) => CompareTo(other.UnderlyingVersion); | |||
/// <summary> | |||
/// Performs a strict equality check between <see langword="this"/> and <paramref name="obj"/>. | |||
/// </summary> | |||
/// <remarks> | |||
/// This may return <see langword="false"/> where <see cref="operator ==(AlmostVersion, AlmostVersion)"/> returns <see langword="true"/> | |||
/// </remarks> | |||
/// <param name="obj">the object to compare to</param> | |||
/// <returns><see langword="true"/> if they are equal, <see langword="false"/> otherwise</returns> | |||
/// <seealso cref="object.Equals(object)"/> | |||
public override bool Equals(object? obj) | |||
{ | |||
return obj is AlmostVersion version && | |||
SemverValue == version.SemverValue && | |||
StringValue == version.StringValue && | |||
StorageMode == version.StorageMode; | |||
} | |||
/// <summary> | |||
/// Default generated hash code function generated by VS. | |||
/// </summary> | |||
/// <returns>a value unique to each object, except those that are considered equal by <see cref="Equals(object)"/></returns> | |||
/// <seealso cref="object.GetHashCode"/> | |||
public override int GetHashCode() | |||
{ | |||
var hashCode = -126402897; | |||
hashCode = (hashCode * -1521134295) + EqualityComparer<Version?>.Default.GetHashCode(SemverValue); | |||
hashCode = (hashCode * -1521134295) + EqualityComparer<string?>.Default.GetHashCode(StringValue); | |||
hashCode = (hashCode * -1521134295) + StorageMode.GetHashCode(); | |||
return hashCode; | |||
} | |||
/// <summary> | |||
/// Compares two versions, only taking into account the numeric part of the version if they are stored as <see cref="Version"/>s, | |||
/// or strict equality if they are stored as <see cref="string"/>s. | |||
/// </summary> | |||
/// <remarks> | |||
/// This is a looser equality than <see cref="Equals(object)"/>, meaning that this may return <see langword="true"/> where <see cref="Equals(object)"/> | |||
/// does not. | |||
/// </remarks> | |||
/// <param name="l">the first value to compare</param> | |||
/// <param name="r">the second value to compare</param> | |||
/// <returns><see langword="true"/> if they are mostly equal, <see langword="false"/> otherwise</returns> | |||
/// <seealso cref="Equals(object)"/> | |||
public static bool operator==(AlmostVersion l, AlmostVersion r) | |||
{ | |||
if (l is null && r is null) return true; | |||
if (l is null || r is null) return false; | |||
if (l.StorageMode != r.StorageMode) return false; | |||
return l.StorageMode == StoredAs.SemVer | |||
? Utils.VersionCompareNoPrerelease(l.SemverValue!, r.SemverValue!) == 0 | |||
: l.StringValue == r.StringValue; | |||
} | |||
/// <summary> | |||
/// The opposite of <see cref="operator ==(AlmostVersion, AlmostVersion)"/>. Equivalent to <c>!(l == r)</c>. | |||
/// </summary> | |||
/// <param name="l">the first value to compare</param> | |||
/// <param name="r">the second value to compare</param> | |||
/// <returns><see langword="true"/> if they are not mostly equal, <see langword="false"/> otherwise</returns> | |||
/// <seealso cref="operator ==(AlmostVersion, AlmostVersion)"/> | |||
public static bool operator!=(AlmostVersion l, AlmostVersion r) => !(l == r); | |||
// implicitly convertible from Version | |||
#pragma warning disable CS0618 // Type or member is obsolete | |||
#pragma warning disable CA2225 // Operator overloads have named alternates | |||
/// <summary> | |||
/// Implicitly converts a <see cref="SVersion"/> to <see cref="AlmostVersion"/> using <see cref="AlmostVersion(SVersion)"/>. | |||
/// </summary> | |||
/// <param name="ver">the <see cref="SVersion"/> to convert</param> | |||
/// <seealso cref="AlmostVersion(SVersion)"/> | |||
[Obsolete("Use Hive.Versioning.Version instead of SemVer.Version")] | |||
public static implicit operator AlmostVersion?(SVersion? ver) => ver is null ? null : new(ver); | |||
// implicitly convertible to Version | |||
/// <summary> | |||
/// Implicitly converts an <see cref="AlmostVersion"/> to <see cref="SVersion"/>, if applicable, using <see cref="SemverValue"/>. | |||
/// If not applicable, returns <see langword="null"/> | |||
/// </summary> | |||
/// <param name="av">the <see cref="AlmostVersion"/> to convert to a <see cref="SVersion"/></param> | |||
/// <seealso cref="SemverValue"/> | |||
[Obsolete("Use Hive.Versioning.Version instead of SemVer.Version")] | |||
public static implicit operator SVersion?(AlmostVersion? av) => av?.SemverValue is not null ? SVersion.ForHiveVersion(av.SemverValue) : null; | |||
#pragma warning restore CS0618 // Type or member is obsolete | |||
/// <summary> | |||
/// Implicitly converts a <see cref="SVersion"/> to <see cref="AlmostVersion"/> using <see cref="AlmostVersion(SVersion)"/>. | |||
/// </summary> | |||
/// <param name="ver">the <see cref="SVersion"/> to convert</param> | |||
/// <seealso cref="AlmostVersion(SVersion)"/> | |||
public static implicit operator AlmostVersion?(Version? ver) => ver is null ? null : new(ver); | |||
// implicitly convertible to Version | |||
/// <summary> | |||
/// Implicitly converts an <see cref="AlmostVersion"/> to <see cref="SVersion"/>, if applicable, using <see cref="SemverValue"/>. | |||
/// If not applicable, returns <see langword="null"/> | |||
/// </summary> | |||
/// <param name="av">the <see cref="AlmostVersion"/> to convert to a <see cref="SVersion"/></param> | |||
/// <seealso cref="SemverValue"/> | |||
public static implicit operator Version?(AlmostVersion av) => av?.SemverValue; | |||
#pragma warning restore CA2225 // Operator overloads have named alternates | |||
public static bool operator <(AlmostVersion left, AlmostVersion right) | |||
=> left is null ? right is not null : left.CompareTo(right) < 0; | |||
public static bool operator <=(AlmostVersion left, AlmostVersion right) | |||
=> left is null || left.CompareTo(right) <= 0; | |||
public static bool operator >(AlmostVersion left, AlmostVersion right) | |||
=> left is not null && left.CompareTo(right) > 0; | |||
public static bool operator >=(AlmostVersion left, AlmostVersion right) | |||
=> left is null ? right is null : left.CompareTo(right) >= 0; | |||
} | |||
/// <summary> | |||
/// A <see cref="ValueConverter{T}"/> for <see cref="AlmostVersion"/>s. | |||
/// </summary> | |||
public sealed class AlmostVersionConverter : ValueConverter<AlmostVersion> | |||
{ | |||
/// <summary> | |||
/// Converts a <see cref="Text"/> node into an <see cref="AlmostVersion"/>. | |||
/// </summary> | |||
/// <param name="value">the <see cref="Text"/> node to convert</param> | |||
/// <param name="parent">the owner of the new object</param> | |||
/// <returns></returns> | |||
public override AlmostVersion? FromValue(Value? value, object parent) | |||
=> Converter<string>.Default.FromValue(value, parent) switch | |||
{ | |||
{ } v => new(v), | |||
_ => null | |||
}; | |||
/// <summary> | |||
/// Converts an <see cref="AlmostVersion"/> to a <see cref="Text"/> node. | |||
/// </summary> | |||
/// <param name="obj">the <see cref="AlmostVersion"/> to convert</param> | |||
/// <param name="parent">the parent of <paramref name="obj"/></param> | |||
/// <returns>a <see cref="Text"/> node representing <paramref name="obj"/></returns> | |||
public override Value? ToValue(AlmostVersion? obj, object parent) | |||
=> Value.From(obj?.ToString()); | |||
} | |||
} |
@ -1,10 +0,0 @@ | |||
#if NET461 | |||
namespace System.Diagnostics.CodeAnalysis | |||
{ | |||
[AttributeUsage(AttributeTargets.Method, Inherited = false)] | |||
public sealed class DoesNotReturnAttribute : Attribute | |||
{ | |||
public DoesNotReturnAttribute() { } | |||
} | |||
} | |||
#endif |
@ -1,3 +1,3 @@ | |||
# ![BSIPA](docs/images/banner_dark.svg) [![Build](https://github.com/bsmg/BeatSaber-IPA-Reloaded/workflows/Build/badge.svg)](https://github.com/bsmg/BeatSaber-IPA-Reloaded) | |||
# ![BSIPA](docs/images/banner_dark.svg) [![Build](https://github.com/nike4613/BeatSaber-IPA-Reloaded/workflows/Build/badge.svg)](https://github.com/nike4613/BeatSaber-IPA-Reloaded) | |||
[ALL DOCUMENTATION HAS MOVED HERE](https://bsmg.github.io/BeatSaber-IPA-Reloaded/) | |||
[ALL DOCUMENTATION HAS MOVED HERE](https://nike4613.github.io/BeatSaber-IPA-Reloaded/) |