mirror of
https://github.com/space-wizards/RobustToolbox.git
synced 2026-02-15 03:30:53 +01:00
Make AssemblyTypeChecker.Types use records, add whitelist dumper command.
This commit is contained in:
35
Robust.Client/Console/Commands/DumpMetadataMembersCommand.cs
Normal file
35
Robust.Client/Console/Commands/DumpMetadataMembersCommand.cs
Normal file
@@ -0,0 +1,35 @@
|
||||
using System;
|
||||
using Robust.Client.Interfaces.Console;
|
||||
using Robust.Shared.ContentPack;
|
||||
using Robust.Shared.Maths;
|
||||
|
||||
namespace Robust.Client.Console.Commands
|
||||
{
|
||||
#if DEBUG
|
||||
internal sealed class DumpMetadataMembersCommand : IConsoleCommand
|
||||
{
|
||||
public string Command => "dmetamem";
|
||||
public string Description => "Dumps a type's members in a format suitable for the sandbox configuration file.";
|
||||
public string Help => "Usage: dmetamem <type>";
|
||||
|
||||
public bool Execute(IDebugConsole console, params string[] args)
|
||||
{
|
||||
var type = Type.GetType(args[0]);
|
||||
|
||||
if (type == null)
|
||||
{
|
||||
console.AddLine("That type does not exist", Color.Red);
|
||||
return false;
|
||||
}
|
||||
|
||||
foreach (var sig in AssemblyTypeChecker.DumpMetaMembers(type))
|
||||
{
|
||||
System.Console.WriteLine(@$"- ""{sig}""");
|
||||
console.AddLine(sig);
|
||||
}
|
||||
|
||||
return false;
|
||||
}
|
||||
}
|
||||
#endif
|
||||
}
|
||||
86
Robust.Shared/ContentPack/AssemblyTypeChecker.Dump.cs
Normal file
86
Robust.Shared/ContentPack/AssemblyTypeChecker.Dump.cs
Normal file
@@ -0,0 +1,86 @@
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.IO;
|
||||
using System.Linq;
|
||||
using System.Reflection;
|
||||
using System.Reflection.Metadata;
|
||||
using System.Reflection.PortableExecutable;
|
||||
using Internal.TypeSystem.Ecma;
|
||||
|
||||
namespace Robust.Shared.ContentPack
|
||||
{
|
||||
internal sealed partial class AssemblyTypeChecker
|
||||
{
|
||||
public static IEnumerable<string> DumpMetaMembers(Type type)
|
||||
{
|
||||
var assemblyLoc = type.Assembly.Location;
|
||||
|
||||
// Load assembly with System.Reflection.Metadata.
|
||||
using var fs = File.OpenRead(assemblyLoc);
|
||||
using var peReader = new PEReader(fs);
|
||||
|
||||
var metaReader = peReader.GetMetadataReader();
|
||||
|
||||
// Find type definition in raw assembly metadata.
|
||||
// Is there a better way to do this than iterating??
|
||||
TypeDefinition typeDef = default;
|
||||
var found = false;
|
||||
foreach (var typeDefHandle in metaReader.TypeDefinitions)
|
||||
{
|
||||
var tempTypeDef = metaReader.GetTypeDefinition(typeDefHandle);
|
||||
var name = metaReader.GetString(tempTypeDef.Name);
|
||||
var @namespace = NilNullString(metaReader, tempTypeDef.Namespace);
|
||||
if (name == type.Name && @namespace == type.Namespace)
|
||||
{
|
||||
typeDef = tempTypeDef;
|
||||
found = true;
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
if (!found)
|
||||
{
|
||||
throw new InvalidOperationException("Type didn't exist??");
|
||||
}
|
||||
|
||||
// Dump the list.
|
||||
var provider = new TypeProvider();
|
||||
|
||||
foreach (var fieldHandle in typeDef.GetFields())
|
||||
{
|
||||
var fieldDef = metaReader.GetFieldDefinition(fieldHandle);
|
||||
|
||||
if ((fieldDef.Attributes & FieldAttributes.FieldAccessMask) != FieldAttributes.Public)
|
||||
{
|
||||
continue;
|
||||
}
|
||||
|
||||
var fieldName = metaReader.GetString(fieldDef.Name);
|
||||
var fieldType = fieldDef.DecodeSignature(provider, 0);
|
||||
|
||||
yield return $"{fieldType.WhitelistToString()} {fieldName}";
|
||||
}
|
||||
|
||||
foreach (var methodHandle in typeDef.GetMethods())
|
||||
{
|
||||
var methodDef = metaReader.GetMethodDefinition(methodHandle);
|
||||
|
||||
if (!methodDef.Attributes.IsPublic())
|
||||
{
|
||||
continue;
|
||||
}
|
||||
|
||||
var methodName = metaReader.GetString(methodDef.Name);
|
||||
var methodSig = methodDef.DecodeSignature(provider, 0);
|
||||
|
||||
var paramString = string.Join(", ", methodSig.ParameterTypes.Select(t => t.WhitelistToString()));
|
||||
var genericCount = methodSig.GenericParameterCount;
|
||||
var typeParamString = genericCount == 0
|
||||
? ""
|
||||
: $"<{new string(',', genericCount - 1)}>";
|
||||
|
||||
yield return $"{methodSig.ReturnType.WhitelistToString()} {methodName}{typeParamString}({paramString})";
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1,21 +1,14 @@
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Collections.Immutable;
|
||||
using System.Diagnostics;
|
||||
using System.Collections.Immutable;
|
||||
using System.Linq;
|
||||
using System.Reflection.Metadata;
|
||||
// ReSharper disable MemberCanBePrivate.Global
|
||||
|
||||
namespace Robust.Shared.ContentPack
|
||||
{
|
||||
internal sealed partial class AssemblyTypeChecker
|
||||
{
|
||||
internal abstract class MType
|
||||
internal abstract record MType
|
||||
{
|
||||
public virtual IEnumerable<MType> GetUsedTypes()
|
||||
{
|
||||
return Array.Empty<MType>();
|
||||
}
|
||||
|
||||
public virtual bool WhitelistEquals(MType other)
|
||||
{
|
||||
return false;
|
||||
@@ -25,6 +18,14 @@ namespace Robust.Shared.ContentPack
|
||||
{
|
||||
return false;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Outputs this type in a format re-parseable for the sandbox config whitelist.
|
||||
/// </summary>
|
||||
public virtual string? WhitelistToString()
|
||||
{
|
||||
return ToString();
|
||||
}
|
||||
}
|
||||
|
||||
internal abstract class MMemberRef
|
||||
@@ -74,17 +75,8 @@ namespace Robust.Shared.ContentPack
|
||||
}
|
||||
}
|
||||
|
||||
internal sealed class MTypeParsed : MType
|
||||
internal sealed record MTypeParsed(string FullName, MTypeParsed? NestedParent = null) : MType
|
||||
{
|
||||
public readonly string FullName;
|
||||
public readonly MTypeParsed? NestedParent;
|
||||
|
||||
public MTypeParsed(string fullName, MTypeParsed? nestedParent = null)
|
||||
{
|
||||
FullName = fullName;
|
||||
NestedParent = nestedParent;
|
||||
}
|
||||
|
||||
public override string ToString()
|
||||
{
|
||||
return NestedParent != null ? $"{NestedParent}/{FullName}" : FullName;
|
||||
@@ -122,32 +114,10 @@ namespace Robust.Shared.ContentPack
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
private bool Equals(MTypeParsed other)
|
||||
{
|
||||
return FullName == other.FullName;
|
||||
}
|
||||
|
||||
public override bool Equals(object? obj)
|
||||
{
|
||||
return ReferenceEquals(this, obj) || obj is MTypeParsed other && Equals(other);
|
||||
}
|
||||
|
||||
public override int GetHashCode()
|
||||
{
|
||||
return FullName.GetHashCode();
|
||||
}
|
||||
}
|
||||
|
||||
internal sealed class MTypePrimitive : MType
|
||||
internal sealed record MTypePrimitive(PrimitiveTypeCode TypeCode) : MType
|
||||
{
|
||||
public readonly PrimitiveTypeCode TypeCode;
|
||||
|
||||
public MTypePrimitive(PrimitiveTypeCode typeCode)
|
||||
{
|
||||
TypeCode = typeCode;
|
||||
}
|
||||
|
||||
public override string ToString()
|
||||
{
|
||||
return TypeCode switch
|
||||
@@ -166,6 +136,7 @@ namespace Robust.Shared.ContentPack
|
||||
PrimitiveTypeCode.Single => "float32",
|
||||
PrimitiveTypeCode.Double => "float64",
|
||||
PrimitiveTypeCode.String => "string",
|
||||
// ReSharper disable once StringLiteralTypo
|
||||
PrimitiveTypeCode.TypedReference => "typedref",
|
||||
PrimitiveTypeCode.IntPtr => "native int",
|
||||
PrimitiveTypeCode.UIntPtr => "unsigned native int",
|
||||
@@ -174,14 +145,33 @@ namespace Robust.Shared.ContentPack
|
||||
};
|
||||
}
|
||||
|
||||
public override bool Equals(object? obj)
|
||||
public override string WhitelistToString()
|
||||
{
|
||||
return obj is MTypePrimitive prim && prim.TypeCode == TypeCode;
|
||||
}
|
||||
|
||||
public override int GetHashCode()
|
||||
{
|
||||
return (int) TypeCode;
|
||||
return TypeCode switch
|
||||
{
|
||||
PrimitiveTypeCode.Void => "void",
|
||||
PrimitiveTypeCode.Boolean => "bool",
|
||||
PrimitiveTypeCode.Char => "char",
|
||||
PrimitiveTypeCode.SByte => "sbyte",
|
||||
PrimitiveTypeCode.Byte => "byte",
|
||||
PrimitiveTypeCode.Int16 => "short",
|
||||
PrimitiveTypeCode.UInt16 => "ushort",
|
||||
PrimitiveTypeCode.Int32 => "int",
|
||||
PrimitiveTypeCode.UInt32 => "uint",
|
||||
PrimitiveTypeCode.Int64 => "long",
|
||||
PrimitiveTypeCode.UInt64 => "ulong",
|
||||
PrimitiveTypeCode.Single => "float",
|
||||
PrimitiveTypeCode.Double => "double",
|
||||
PrimitiveTypeCode.String => "string",
|
||||
// ReSharper disable once StringLiteralTypo
|
||||
PrimitiveTypeCode.TypedReference => "typedref",
|
||||
// ReSharper disable once StringLiteralTypo
|
||||
PrimitiveTypeCode.IntPtr => "nint",
|
||||
// ReSharper disable once StringLiteralTypo
|
||||
PrimitiveTypeCode.UIntPtr => "unint",
|
||||
PrimitiveTypeCode.Object => "object",
|
||||
_ => "???"
|
||||
};
|
||||
}
|
||||
|
||||
public override bool WhitelistEquals(MType other)
|
||||
@@ -190,52 +180,36 @@ namespace Robust.Shared.ContentPack
|
||||
}
|
||||
}
|
||||
|
||||
internal sealed class MTypeSZArray : MType
|
||||
internal sealed record MTypeSZArray(MType ElementType) : MType
|
||||
{
|
||||
public readonly MType ElementType;
|
||||
|
||||
public MTypeSZArray(MType elementType)
|
||||
{
|
||||
ElementType = elementType;
|
||||
}
|
||||
|
||||
public override IEnumerable<MType> GetUsedTypes()
|
||||
{
|
||||
return new[] {ElementType};
|
||||
}
|
||||
|
||||
public override string ToString()
|
||||
{
|
||||
return $"{ElementType}[]";
|
||||
}
|
||||
|
||||
public override string WhitelistToString()
|
||||
{
|
||||
return $"{ElementType.WhitelistToString()}[]";
|
||||
}
|
||||
|
||||
public override bool WhitelistEquals(MType other)
|
||||
{
|
||||
return other is MTypeSZArray arr && ElementType.WhitelistEquals(arr.ElementType);
|
||||
}
|
||||
}
|
||||
|
||||
internal sealed class MTypeArray : MType
|
||||
internal sealed record MTypeArray(MType ElementType, ArrayShape Shape) : MType
|
||||
{
|
||||
public readonly MType ElementType;
|
||||
public readonly ArrayShape Shape;
|
||||
|
||||
public MTypeArray(MType elementType, ArrayShape shape)
|
||||
{
|
||||
ElementType = elementType;
|
||||
Shape = shape;
|
||||
}
|
||||
|
||||
public override IEnumerable<MType> GetUsedTypes()
|
||||
{
|
||||
return new[] {ElementType};
|
||||
}
|
||||
|
||||
public override string ToString()
|
||||
{
|
||||
return $"{ElementType}[TODO]";
|
||||
}
|
||||
|
||||
public override string WhitelistToString()
|
||||
{
|
||||
return $"{ElementType.WhitelistToString()}[TODO]";
|
||||
}
|
||||
|
||||
public override bool WhitelistEquals(MType other)
|
||||
{
|
||||
return other is MTypeArray arr && ShapesEqual(Shape, arr.Shape) && ElementType.WhitelistEquals(arr);
|
||||
@@ -252,48 +226,34 @@ namespace Robust.Shared.ContentPack
|
||||
}
|
||||
}
|
||||
|
||||
internal sealed class MTypeByRef : MType
|
||||
internal sealed record MTypeByRef(MType ElementType) : MType
|
||||
{
|
||||
public readonly MType ElementType;
|
||||
|
||||
public MTypeByRef(MType elementType)
|
||||
{
|
||||
ElementType = elementType;
|
||||
}
|
||||
|
||||
public override IEnumerable<MType> GetUsedTypes()
|
||||
{
|
||||
return new[] {ElementType};
|
||||
}
|
||||
|
||||
public override string ToString()
|
||||
{
|
||||
return $"{ElementType}&";
|
||||
}
|
||||
|
||||
public override string WhitelistToString()
|
||||
{
|
||||
return $"ref {ElementType.WhitelistToString()}";
|
||||
}
|
||||
|
||||
public override bool WhitelistEquals(MType other)
|
||||
{
|
||||
return other is MTypeByRef byRef && ElementType.WhitelistEquals(byRef.ElementType);
|
||||
}
|
||||
}
|
||||
|
||||
internal sealed class MTypePointer : MType
|
||||
internal sealed record MTypePointer(MType ElementType) : MType
|
||||
{
|
||||
public MType ElementType { get; }
|
||||
|
||||
public MTypePointer(MType elementType)
|
||||
{
|
||||
ElementType = elementType;
|
||||
}
|
||||
|
||||
public override IEnumerable<MType> GetUsedTypes()
|
||||
{
|
||||
return new[] {ElementType};
|
||||
}
|
||||
|
||||
public override string ToString()
|
||||
{
|
||||
return $"{base.ToString()}*";
|
||||
return $"{ElementType}*";
|
||||
}
|
||||
|
||||
public override string WhitelistToString()
|
||||
{
|
||||
return $"{ElementType.WhitelistToString()}*";
|
||||
}
|
||||
|
||||
public override bool WhitelistEquals(MType other)
|
||||
@@ -302,32 +262,18 @@ namespace Robust.Shared.ContentPack
|
||||
}
|
||||
}
|
||||
|
||||
internal sealed class MTypeGeneric : MType
|
||||
internal sealed record MTypeGeneric(MType GenericType, ImmutableArray<MType> TypeArguments) : MType
|
||||
{
|
||||
public MType GenericType { get; }
|
||||
public ImmutableArray<MType> TypeArguments { get; }
|
||||
|
||||
public MTypeGeneric(MType genericType, ImmutableArray<MType> typeArguments)
|
||||
{
|
||||
GenericType = genericType;
|
||||
TypeArguments = typeArguments;
|
||||
}
|
||||
|
||||
public override IEnumerable<MType> GetUsedTypes()
|
||||
{
|
||||
yield return GenericType;
|
||||
|
||||
foreach (var typeArgument in TypeArguments)
|
||||
{
|
||||
yield return typeArgument;
|
||||
}
|
||||
}
|
||||
|
||||
public override string ToString()
|
||||
{
|
||||
return $"{GenericType}<{string.Join(", ", TypeArguments)}>";
|
||||
}
|
||||
|
||||
public override string WhitelistToString()
|
||||
{
|
||||
return $"{GenericType.WhitelistToString()}<{string.Join(", ", TypeArguments.Select(t => t.WhitelistToString()))}>";
|
||||
}
|
||||
|
||||
public override bool WhitelistEquals(MType other)
|
||||
{
|
||||
if (!(other is MTypeGeneric generic))
|
||||
@@ -354,48 +300,20 @@ namespace Robust.Shared.ContentPack
|
||||
return GenericType.WhitelistEquals(generic.GenericType);
|
||||
}
|
||||
|
||||
public bool Equals(MTypeGeneric? otherGeneric)
|
||||
{
|
||||
return otherGeneric != null && GenericType.Equals(otherGeneric.GenericType) &&
|
||||
TypeArguments.SequenceEqual(otherGeneric.TypeArguments);
|
||||
}
|
||||
|
||||
public override bool IsCoreTypeDefined()
|
||||
{
|
||||
return GenericType.IsCoreTypeDefined();
|
||||
}
|
||||
|
||||
private bool Equals(MTypeGeneric other)
|
||||
{
|
||||
return GenericType.Equals(other.GenericType) && TypeArguments.SequenceEqual(other.TypeArguments);
|
||||
}
|
||||
|
||||
public override bool Equals(object? obj)
|
||||
{
|
||||
return obj is MTypeGeneric other && Equals(other);
|
||||
}
|
||||
|
||||
public override int GetHashCode()
|
||||
{
|
||||
var hc = new HashCode();
|
||||
hc.Add(GenericType);
|
||||
hc.Add(TypeArguments.Length);
|
||||
foreach (var typeArg in TypeArguments)
|
||||
{
|
||||
hc.Add(typeArg);
|
||||
}
|
||||
|
||||
return hc.ToHashCode();
|
||||
}
|
||||
}
|
||||
|
||||
internal sealed class MTypeDefined : MType
|
||||
internal sealed record MTypeDefined(string Name, string? Namespace, MTypeDefined? Enclosing) : MType
|
||||
{
|
||||
public string Name { get; }
|
||||
public string? Namespace { get; }
|
||||
public MTypeDefined? Enclosing { get; }
|
||||
|
||||
public MTypeDefined(string name, string? ns, MTypeDefined? enclosing)
|
||||
{
|
||||
Name = name;
|
||||
Namespace = ns;
|
||||
Enclosing = enclosing;
|
||||
}
|
||||
|
||||
public override string ToString()
|
||||
{
|
||||
var name = Namespace != null ? $"{Namespace}.{Name}" : Name;
|
||||
@@ -414,29 +332,26 @@ namespace Robust.Shared.ContentPack
|
||||
}
|
||||
}
|
||||
|
||||
internal sealed class MTypeReferenced : MType
|
||||
internal sealed record MTypeReferenced(MResScope ResolutionScope, string Name, string? Namespace) : MType
|
||||
{
|
||||
public MResScope ResolutionScope { get; }
|
||||
public string Name { get; }
|
||||
public string? Namespace { get; }
|
||||
|
||||
public MTypeReferenced(MResScope resolutionScope, string name, string? @namespace)
|
||||
{
|
||||
ResolutionScope = resolutionScope;
|
||||
Name = name;
|
||||
Namespace = @namespace;
|
||||
}
|
||||
|
||||
public override string ToString()
|
||||
{
|
||||
if (Namespace == null)
|
||||
{
|
||||
return $"{ResolutionScope}{Name}";
|
||||
}
|
||||
else
|
||||
|
||||
return $"{ResolutionScope}{Namespace}.{Name}";
|
||||
}
|
||||
|
||||
public override string WhitelistToString()
|
||||
{
|
||||
if (Namespace == null)
|
||||
{
|
||||
return $"{ResolutionScope}{Namespace}.{Name}";
|
||||
return Name;
|
||||
}
|
||||
|
||||
return $"{Namespace}.{Name}";
|
||||
}
|
||||
|
||||
public override bool WhitelistEquals(MType other)
|
||||
@@ -453,129 +368,65 @@ namespace Robust.Shared.ContentPack
|
||||
}
|
||||
}
|
||||
|
||||
internal abstract class MResScope
|
||||
internal abstract record MResScope
|
||||
{
|
||||
}
|
||||
|
||||
internal sealed class MResScopeType : MResScope
|
||||
internal sealed record MResScopeType(MType Type) : MResScope
|
||||
{
|
||||
public MType Type { get; }
|
||||
|
||||
public MResScopeType(MType type)
|
||||
{
|
||||
Type = type;
|
||||
}
|
||||
|
||||
public override string ToString()
|
||||
{
|
||||
return $"{Type}/";
|
||||
}
|
||||
}
|
||||
|
||||
internal sealed class MResScopeAssembly : MResScope
|
||||
internal sealed record MResScopeAssembly(string Name) : MResScope
|
||||
{
|
||||
public string Name { get; }
|
||||
|
||||
public MResScopeAssembly(string name)
|
||||
{
|
||||
Name = name;
|
||||
}
|
||||
|
||||
public override string ToString()
|
||||
{
|
||||
return $"[{Name}]";
|
||||
}
|
||||
}
|
||||
|
||||
internal sealed class MTypeGenericTypePlaceHolder : MType
|
||||
internal sealed record MTypeGenericTypePlaceHolder(int Index) : MType
|
||||
{
|
||||
public int Index { get; }
|
||||
|
||||
public MTypeGenericTypePlaceHolder(int index)
|
||||
{
|
||||
Index = index;
|
||||
}
|
||||
|
||||
public override string ToString()
|
||||
{
|
||||
return $"!{Index}";
|
||||
}
|
||||
|
||||
private bool Equals(MTypeGenericTypePlaceHolder other)
|
||||
{
|
||||
return Index == other.Index;
|
||||
}
|
||||
|
||||
public override bool Equals(object? obj)
|
||||
{
|
||||
return ReferenceEquals(this, obj) || obj is MTypeGenericTypePlaceHolder other && Equals(other);
|
||||
}
|
||||
|
||||
public override bool WhitelistEquals(MType other)
|
||||
{
|
||||
return Equals(other);
|
||||
}
|
||||
|
||||
public override int GetHashCode()
|
||||
{
|
||||
return Index;
|
||||
}
|
||||
}
|
||||
|
||||
internal sealed class MTypeGenericMethodPlaceHolder : MType
|
||||
internal sealed record MTypeGenericMethodPlaceHolder(int Index) : MType
|
||||
{
|
||||
public int Index { get; }
|
||||
|
||||
public MTypeGenericMethodPlaceHolder(int index)
|
||||
{
|
||||
Index = index;
|
||||
}
|
||||
|
||||
public override string ToString()
|
||||
{
|
||||
return $"!!{Index}";
|
||||
}
|
||||
|
||||
private bool Equals(MTypeGenericMethodPlaceHolder other)
|
||||
{
|
||||
return Index == other.Index;
|
||||
}
|
||||
|
||||
public override bool Equals(object? obj)
|
||||
{
|
||||
return ReferenceEquals(this, obj) || obj is MTypeGenericMethodPlaceHolder other && Equals(other);
|
||||
}
|
||||
|
||||
public override bool WhitelistEquals(MType other)
|
||||
{
|
||||
return Equals(other);
|
||||
}
|
||||
|
||||
public override int GetHashCode()
|
||||
{
|
||||
return Index;
|
||||
}
|
||||
}
|
||||
|
||||
internal sealed class MTypeModified : MType
|
||||
internal sealed record MTypeModified(MType UnmodifiedType, MType ModifierType, bool Required) : MType
|
||||
{
|
||||
public MType UnmodifiedType { get; }
|
||||
public MType ModifierType { get; }
|
||||
public bool Required { get; }
|
||||
|
||||
public MTypeModified(MType unmodifiedType, MType modifierType, bool required)
|
||||
{
|
||||
UnmodifiedType = unmodifiedType;
|
||||
ModifierType = modifierType;
|
||||
Required = required;
|
||||
}
|
||||
|
||||
public override string ToString()
|
||||
{
|
||||
var modName = Required ? "modreq" : "modopt";
|
||||
return $"{UnmodifiedType} {modName}({ModifierType})";
|
||||
}
|
||||
|
||||
public override string? WhitelistToString()
|
||||
{
|
||||
return UnmodifiedType.WhitelistToString();
|
||||
}
|
||||
|
||||
public override bool WhitelistEquals(MType other)
|
||||
{
|
||||
// TODO: This is asymmetric shit.
|
||||
|
||||
Reference in New Issue
Block a user