@ -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"?> | <?xml version="1.0" encoding="utf-8"?> | ||||
<Project> | <Project> | ||||
<ItemGroup> | |||||
<Compile Include="$(MSBuildThisFileDirectory)\System.Diagnostics.CodeAnalysis.cs"/> | |||||
</ItemGroup> | |||||
</Project> | </Project> |
@ -1,73 +1,73 @@ | |||||
using IPA.Logging; | using IPA.Logging; | ||||
using System; | |||||
using System; | |||||
using System.Diagnostics; | using System.Diagnostics; | ||||
using System.Diagnostics.CodeAnalysis; | 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", | [SuppressMessage("Reliability", "CA2008:Do not create tasks without passing a TaskScheduler", | ||||
Justification = "I very explicitly want the default scheduler")] | 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)] | [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)) | 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); | 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/) |