mirror of
https://github.com/aaru-dps/Aaru.git
synced 2025-12-16 19:24:25 +00:00
Added support for SecureDigital / MultiMediaCard on Windows.
This commit is contained in:
@@ -275,7 +275,7 @@ namespace DiscImageChef.Devices
|
|||||||
{
|
{
|
||||||
case Interop.PlatformID.Win32NT:
|
case Interop.PlatformID.Win32NT:
|
||||||
{
|
{
|
||||||
throw new NotImplementedException();
|
return Windows.Command.SendMmcCommand((SafeFileHandle)fd, command, write, isApplication, flags, argument, blockSize, blocks, ref buffer, out response, out duration, out sense, timeout);
|
||||||
}
|
}
|
||||||
case Interop.PlatformID.Linux:
|
case Interop.PlatformID.Linux:
|
||||||
{
|
{
|
||||||
|
|||||||
@@ -203,6 +203,74 @@ namespace DiscImageChef.Devices
|
|||||||
ntDevicePath = Windows.Command.GetDevicePath((SafeFileHandle)fd);
|
ntDevicePath = Windows.Command.GetDevicePath((SafeFileHandle)fd);
|
||||||
DicConsole.DebugWriteLine("Windows devices", "NT device path: {0}", ntDevicePath);
|
DicConsole.DebugWriteLine("Windows devices", "NT device path: {0}", ntDevicePath);
|
||||||
Marshal.FreeHGlobal(descriptorPtr);
|
Marshal.FreeHGlobal(descriptorPtr);
|
||||||
|
|
||||||
|
if(Windows.Command.IsSdhci((SafeFileHandle)fd))
|
||||||
|
{
|
||||||
|
byte[] sdBuffer = new byte[16];
|
||||||
|
bool sense = false;
|
||||||
|
|
||||||
|
lastError = Windows.Command.SendMmcCommand((SafeFileHandle)fd, MmcCommands.SendCSD, false, false, MmcFlags.ResponseSPI_R2 | MmcFlags.Response_R2 | MmcFlags.CommandAC,
|
||||||
|
0, 16, 1, ref sdBuffer, out uint[] response, out double duration, out sense, 0);
|
||||||
|
|
||||||
|
if(!sense)
|
||||||
|
{
|
||||||
|
cachedCsd = new byte[16];
|
||||||
|
Array.Copy(sdBuffer, 0, cachedCsd, 0, 16);
|
||||||
|
}
|
||||||
|
|
||||||
|
sdBuffer = new byte[16];
|
||||||
|
sense = false;
|
||||||
|
|
||||||
|
lastError = Windows.Command.SendMmcCommand((SafeFileHandle)fd, MmcCommands.SendCID, false, false, MmcFlags.ResponseSPI_R2 | MmcFlags.Response_R2 | MmcFlags.CommandAC,
|
||||||
|
0, 16, 1, ref sdBuffer, out response, out duration, out sense, 0);
|
||||||
|
|
||||||
|
if(!sense)
|
||||||
|
{
|
||||||
|
cachedCid = new byte[16];
|
||||||
|
Array.Copy(sdBuffer, 0, cachedCid, 0, 16);
|
||||||
|
}
|
||||||
|
|
||||||
|
sdBuffer = new byte[8];
|
||||||
|
sense = false;
|
||||||
|
|
||||||
|
lastError = Windows.Command.SendMmcCommand((SafeFileHandle)fd, (MmcCommands)SecureDigitalCommands.SendSCR, false, true, MmcFlags.ResponseSPI_R1 | MmcFlags.Response_R1 | MmcFlags.CommandADTC,
|
||||||
|
0, 8, 1, ref sdBuffer, out response, out duration, out sense, 0);
|
||||||
|
|
||||||
|
if(!sense)
|
||||||
|
{
|
||||||
|
cachedScr = new byte[8];
|
||||||
|
Array.Copy(sdBuffer, 0, cachedScr, 0, 8);
|
||||||
|
}
|
||||||
|
|
||||||
|
if(cachedScr != null)
|
||||||
|
{
|
||||||
|
sdBuffer = new byte[4];
|
||||||
|
sense = false;
|
||||||
|
|
||||||
|
lastError = Windows.Command.SendMmcCommand((SafeFileHandle)fd, (MmcCommands)SecureDigitalCommands.SendOperatingCondition, false, true, MmcFlags.ResponseSPI_R3 | MmcFlags.Response_R3 | MmcFlags.CommandBCR,
|
||||||
|
0, 4, 1, ref sdBuffer, out response, out duration, out sense, 0);
|
||||||
|
|
||||||
|
if(!sense)
|
||||||
|
{
|
||||||
|
cachedScr = new byte[4];
|
||||||
|
Array.Copy(sdBuffer, 0, cachedScr, 0, 4);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
sdBuffer = new byte[4];
|
||||||
|
sense = false;
|
||||||
|
|
||||||
|
lastError = Windows.Command.SendMmcCommand((SafeFileHandle)fd, MmcCommands.SendOpCond, false, true, MmcFlags.ResponseSPI_R3 | MmcFlags.Response_R3 | MmcFlags.CommandBCR,
|
||||||
|
0, 4, 1, ref sdBuffer, out response, out duration, out sense, 0);
|
||||||
|
|
||||||
|
if(!sense)
|
||||||
|
{
|
||||||
|
cachedScr = new byte[4];
|
||||||
|
Array.Copy(sdBuffer, 0, cachedScr, 0, 4);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
else
|
else
|
||||||
{
|
{
|
||||||
@@ -236,34 +304,36 @@ namespace DiscImageChef.Devices
|
|||||||
if(len == 0)
|
if(len == 0)
|
||||||
cachedOcr = null;
|
cachedOcr = null;
|
||||||
}
|
}
|
||||||
|
|
||||||
if(cachedCid != null)
|
|
||||||
{
|
|
||||||
scsiType = Decoders.SCSI.PeripheralDeviceTypes.DirectAccess;
|
|
||||||
removable = false;
|
|
||||||
|
|
||||||
if(cachedScr != null)
|
|
||||||
{
|
|
||||||
type = DeviceType.SecureDigital;
|
|
||||||
Decoders.SecureDigital.CID decoded = Decoders.SecureDigital.Decoders.DecodeCID(cachedCid);
|
|
||||||
manufacturer = Decoders.SecureDigital.VendorString.Prettify(decoded.Manufacturer);
|
|
||||||
model = decoded.ProductName;
|
|
||||||
revision = string.Format("{0:X2}.{1:X2}", (decoded.ProductRevision & 0xF0) >> 4, decoded.ProductRevision & 0x0F);
|
|
||||||
serial = string.Format("{0}", decoded.ProductSerialNumber);
|
|
||||||
}
|
|
||||||
else
|
|
||||||
{
|
|
||||||
type = DeviceType.MMC;
|
|
||||||
Decoders.MMC.CID decoded = Decoders.MMC.Decoders.DecodeCID(cachedCid);
|
|
||||||
manufacturer = Decoders.MMC.VendorString.Prettify(decoded.Manufacturer);
|
|
||||||
model = decoded.ProductName;
|
|
||||||
revision = string.Format("{0:X2}.{1:X2}", (decoded.ProductRevision & 0xF0) >> 4, decoded.ProductRevision & 0x0F);
|
|
||||||
serial = string.Format("{0}", decoded.ProductSerialNumber);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#region SecureDigital / MultiMediaCard
|
||||||
|
if(cachedCid != null)
|
||||||
|
{
|
||||||
|
scsiType = Decoders.SCSI.PeripheralDeviceTypes.DirectAccess;
|
||||||
|
removable = false;
|
||||||
|
|
||||||
|
if(cachedScr != null)
|
||||||
|
{
|
||||||
|
type = DeviceType.SecureDigital;
|
||||||
|
Decoders.SecureDigital.CID decoded = Decoders.SecureDigital.Decoders.DecodeCID(cachedCid);
|
||||||
|
manufacturer = Decoders.SecureDigital.VendorString.Prettify(decoded.Manufacturer);
|
||||||
|
model = decoded.ProductName;
|
||||||
|
revision = string.Format("{0:X2}.{1:X2}", (decoded.ProductRevision & 0xF0) >> 4, decoded.ProductRevision & 0x0F);
|
||||||
|
serial = string.Format("{0}", decoded.ProductSerialNumber);
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
type = DeviceType.MMC;
|
||||||
|
Decoders.MMC.CID decoded = Decoders.MMC.Decoders.DecodeCID(cachedCid);
|
||||||
|
manufacturer = Decoders.MMC.VendorString.Prettify(decoded.Manufacturer);
|
||||||
|
model = decoded.ProductName;
|
||||||
|
revision = string.Format("{0:X2}.{1:X2}", (decoded.ProductRevision & 0xF0) >> 4, decoded.ProductRevision & 0x0F);
|
||||||
|
serial = string.Format("{0}", decoded.ProductSerialNumber);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
#endregion SecureDigital / MultiMediaCard
|
||||||
|
|
||||||
#region USB
|
#region USB
|
||||||
|
|
||||||
if(platformID == Interop.PlatformID.Linux)
|
if(platformID == Interop.PlatformID.Linux)
|
||||||
|
|||||||
@@ -570,6 +570,91 @@ namespace DiscImageChef.Devices.Windows
|
|||||||
Extern.SetupDiDestroyDeviceInfoList(hDevInfo);
|
Extern.SetupDiDestroyDeviceInfoList(hDevInfo);
|
||||||
return null;
|
return null;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
internal static bool IsSdhci(SafeFileHandle fd)
|
||||||
|
{
|
||||||
|
SffdiskQueryDeviceProtocolData queryData1 = new SffdiskQueryDeviceProtocolData();
|
||||||
|
queryData1.size = (ushort)Marshal.SizeOf(queryData1);
|
||||||
|
uint bytesReturned;
|
||||||
|
Extern.DeviceIoControl(fd, WindowsIoctl.IOCTL_SFFDISK_QUERY_DEVICE_PROTOCOL, IntPtr.Zero, 0,
|
||||||
|
ref queryData1, queryData1.size, out bytesReturned, IntPtr.Zero);
|
||||||
|
return queryData1.protocolGuid.Equals(Consts.GUID_SFF_PROTOCOL_SD);
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Sends a MMC/SD command
|
||||||
|
/// </summary>
|
||||||
|
/// <returns>The result of the command.</returns>
|
||||||
|
/// <param name="fd">File handle</param>
|
||||||
|
/// <param name="command">MMC/SD opcode</param>
|
||||||
|
/// <param name="buffer">Buffer for MMC/SD command response</param>
|
||||||
|
/// <param name="timeout">Timeout in seconds</param>
|
||||||
|
/// <param name="duration">Time it took to execute the command in milliseconds</param>
|
||||||
|
/// <param name="sense"><c>True</c> if MMC/SD returned non-OK status</param>
|
||||||
|
/// <param name="write"><c>True</c> if data is sent from host to card</param>
|
||||||
|
/// <param name="isApplication"><c>True</c> if command should be preceded with CMD55</param>
|
||||||
|
/// <param name="flags">Flags indicating kind and place of response</param>
|
||||||
|
/// <param name="blocks">How many blocks to transfer</param>
|
||||||
|
/// <param name="argument">Command argument</param>
|
||||||
|
/// <param name="response">Response registers</param>
|
||||||
|
/// <param name="blockSize">Size of block in bytes</param>
|
||||||
|
internal static int SendMmcCommand(SafeFileHandle fd, MmcCommands command, bool write, bool isApplication,
|
||||||
|
MmcFlags flags, uint argument, uint blockSize, uint blocks, ref byte[] buffer, out uint[] response,
|
||||||
|
out double duration, out bool sense, uint timeout = 0)
|
||||||
|
{
|
||||||
|
SffdiskDeviceCommandData commandData = new SffdiskDeviceCommandData();
|
||||||
|
SdCmdDescriptor commandDescriptor = new SdCmdDescriptor();
|
||||||
|
commandData.size = (ushort)Marshal.SizeOf(commandData);
|
||||||
|
commandData.command = SffdiskDcmd.DeviceCommand;
|
||||||
|
commandData.protocolArgumentSize = (ushort)Marshal.SizeOf(commandDescriptor);
|
||||||
|
commandData.deviceDataBufferSize = blockSize * blocks;
|
||||||
|
commandDescriptor.commandCode = (byte)command;
|
||||||
|
commandDescriptor.cmdClass = isApplication ? SdCommandClass.AppCmd : SdCommandClass.Standard;
|
||||||
|
commandDescriptor.transferDirection = write ? SdTransferDirection.Write : SdTransferDirection.Read;
|
||||||
|
commandDescriptor.transferType = flags.HasFlag(MmcFlags.CommandADTC) ? SdTransferType.SingleBlock : SdTransferType.CmdOnly;
|
||||||
|
commandDescriptor.responseType = 0;
|
||||||
|
|
||||||
|
if(flags.HasFlag(MmcFlags.Response_R1) || flags.HasFlag(MmcFlags.ResponseSPI_R1))
|
||||||
|
commandDescriptor.responseType = SdResponseType.R1;
|
||||||
|
if(flags.HasFlag(MmcFlags.Response_R1b) || flags.HasFlag(MmcFlags.ResponseSPI_R1b))
|
||||||
|
commandDescriptor.responseType = SdResponseType.R1b;
|
||||||
|
if(flags.HasFlag(MmcFlags.Response_R2) || flags.HasFlag(MmcFlags.ResponseSPI_R2))
|
||||||
|
commandDescriptor.responseType = SdResponseType.R2;
|
||||||
|
if(flags.HasFlag(MmcFlags.Response_R3) || flags.HasFlag(MmcFlags.ResponseSPI_R3))
|
||||||
|
commandDescriptor.responseType = SdResponseType.R3;
|
||||||
|
if(flags.HasFlag(MmcFlags.Response_R4) || flags.HasFlag(MmcFlags.ResponseSPI_R4))
|
||||||
|
commandDescriptor.responseType = SdResponseType.R4;
|
||||||
|
if(flags.HasFlag(MmcFlags.Response_R5) || flags.HasFlag(MmcFlags.ResponseSPI_R5))
|
||||||
|
commandDescriptor.responseType = SdResponseType.R5;
|
||||||
|
if(flags.HasFlag(MmcFlags.Response_R6))
|
||||||
|
commandDescriptor.responseType = SdResponseType.R6;
|
||||||
|
|
||||||
|
byte[] command_b = new byte[commandData.size + commandData.protocolArgumentSize + commandData.deviceDataBufferSize];
|
||||||
|
IntPtr hBuf = Marshal.AllocHGlobal(command_b.Length);
|
||||||
|
Marshal.StructureToPtr(commandData, hBuf, true);
|
||||||
|
IntPtr descriptorOffset = new IntPtr(hBuf.ToInt32() + commandData.size);
|
||||||
|
Marshal.StructureToPtr(commandDescriptor, descriptorOffset, true);
|
||||||
|
Marshal.Copy(hBuf, command_b, 0, command_b.Length);
|
||||||
|
Marshal.FreeHGlobal(hBuf);
|
||||||
|
|
||||||
|
uint bytesReturned;
|
||||||
|
int error = 0;
|
||||||
|
DateTime start = DateTime.Now;
|
||||||
|
sense = !Extern.DeviceIoControl(fd, WindowsIoctl.IOCTL_SFFDISK_DEVICE_COMMAND, command_b,
|
||||||
|
(uint)command_b.Length, command_b, (uint)command_b.Length, out bytesReturned, IntPtr.Zero);
|
||||||
|
DateTime end = DateTime.Now;
|
||||||
|
|
||||||
|
if(sense)
|
||||||
|
error = Marshal.GetLastWin32Error();
|
||||||
|
|
||||||
|
buffer = new byte[blockSize * blocks];
|
||||||
|
Buffer.BlockCopy(command_b, command_b.Length - buffer.Length, buffer, 0, buffer.Length);
|
||||||
|
|
||||||
|
response = new uint[4];
|
||||||
|
duration = (end - start).TotalMilliseconds;
|
||||||
|
|
||||||
|
return error;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -343,6 +343,8 @@ namespace DiscImageChef.Devices.Windows
|
|||||||
IOCTL_STORAGE_QUERY_PROPERTY = 0x2D1400,
|
IOCTL_STORAGE_QUERY_PROPERTY = 0x2D1400,
|
||||||
IOCTL_IDE_PASS_THROUGH = 0x4D028,
|
IOCTL_IDE_PASS_THROUGH = 0x4D028,
|
||||||
IOCTL_STORAGE_GET_DEVICE_NUMBER = 0x2D1080,
|
IOCTL_STORAGE_GET_DEVICE_NUMBER = 0x2D1080,
|
||||||
|
IOCTL_SFFDISK_QUERY_DEVICE_PROTOCOL = 0x71E80,
|
||||||
|
IOCTL_SFFDISK_DEVICE_COMMAND = 0x79E84,
|
||||||
}
|
}
|
||||||
|
|
||||||
[Flags]
|
[Flags]
|
||||||
@@ -446,9 +448,54 @@ namespace DiscImageChef.Devices.Windows
|
|||||||
/// </summary>
|
/// </summary>
|
||||||
DeviceInterface = 0x10,
|
DeviceInterface = 0x10,
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public enum SdCommandClass : uint
|
||||||
|
{
|
||||||
|
Standard,
|
||||||
|
AppCmd
|
||||||
|
}
|
||||||
|
|
||||||
|
public enum SdTransferDirection : uint
|
||||||
|
{
|
||||||
|
Unspecified,
|
||||||
|
Read,
|
||||||
|
Write
|
||||||
|
}
|
||||||
|
|
||||||
|
public enum SdTransferType : uint
|
||||||
|
{
|
||||||
|
Unspecified,
|
||||||
|
CmdOnly,
|
||||||
|
SingleBlock,
|
||||||
|
MultiBlock,
|
||||||
|
MultiBlockNoCmd12
|
||||||
|
}
|
||||||
|
|
||||||
|
public enum SdResponseType : uint
|
||||||
|
{
|
||||||
|
Unspecified,
|
||||||
|
None,
|
||||||
|
R1,
|
||||||
|
R1b,
|
||||||
|
R2,
|
||||||
|
R3,
|
||||||
|
R4,
|
||||||
|
R5,
|
||||||
|
R5b,
|
||||||
|
R6
|
||||||
|
}
|
||||||
|
|
||||||
|
public enum SffdiskDcmd : uint
|
||||||
|
{
|
||||||
|
GetVersion,
|
||||||
|
LockChannel,
|
||||||
|
UnlockChannel,
|
||||||
|
DeviceCommand
|
||||||
|
};
|
||||||
|
|
||||||
static class Consts
|
static class Consts
|
||||||
{
|
{
|
||||||
|
public static Guid GUID_SFF_PROTOCOL_SD = new Guid("AD7536A8-D055-4C40-AA4D-96312DDB6B38");
|
||||||
public static Guid GUID_DEVINTERFACE_DISK = new Guid(0x53F56307, 0xB6BF, 0x11D0, 0x94, 0xF2, 0x00, 0xA0, 0xC9, 0x1E, 0xFB, 0x8B);
|
public static Guid GUID_DEVINTERFACE_DISK = new Guid(0x53F56307, 0xB6BF, 0x11D0, 0x94, 0xF2, 0x00, 0xA0, 0xC9, 0x1E, 0xFB, 0x8B);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -98,7 +98,6 @@ namespace DiscImageChef.Devices.Windows
|
|||||||
IntPtr Overlapped
|
IntPtr Overlapped
|
||||||
);
|
);
|
||||||
|
|
||||||
|
|
||||||
[DllImport("Kernel32.dll", SetLastError = true, EntryPoint = "DeviceIoControl", CharSet = CharSet.Auto)]
|
[DllImport("Kernel32.dll", SetLastError = true, EntryPoint = "DeviceIoControl", CharSet = CharSet.Auto)]
|
||||||
internal static extern bool DeviceIoControlGetDeviceNumber(
|
internal static extern bool DeviceIoControlGetDeviceNumber(
|
||||||
SafeFileHandle hDevice,
|
SafeFileHandle hDevice,
|
||||||
@@ -111,6 +110,22 @@ namespace DiscImageChef.Devices.Windows
|
|||||||
IntPtr Overlapped
|
IntPtr Overlapped
|
||||||
);
|
);
|
||||||
|
|
||||||
|
[DllImport("Kernel32.dll", SetLastError = true, EntryPoint = "DeviceIoControl", CharSet = CharSet.Auto)]
|
||||||
|
internal static extern bool DeviceIoControl(
|
||||||
|
SafeFileHandle hDevice,
|
||||||
|
WindowsIoctl IoControlCode,
|
||||||
|
IntPtr InBuffer,
|
||||||
|
uint nInBufferSize,
|
||||||
|
ref SffdiskQueryDeviceProtocolData OutBuffer,
|
||||||
|
uint nOutBufferSize,
|
||||||
|
out uint pBytesReturned,
|
||||||
|
IntPtr Overlapped
|
||||||
|
);
|
||||||
|
|
||||||
|
[DllImport("Kernel32.dll", SetLastError = true, EntryPoint = "DeviceIoControl", CharSet = CharSet.Auto)]
|
||||||
|
internal static extern bool DeviceIoControl(SafeFileHandle hDevice, WindowsIoctl IoControlCode, byte[] InBuffer,
|
||||||
|
uint nInBufferSize, byte[] OutBuffer, uint nOutBufferSize, out uint pBytesReturned, IntPtr Overlapped);
|
||||||
|
|
||||||
[DllImport("setupapi.dll", CharSet = CharSet.Auto)]
|
[DllImport("setupapi.dll", CharSet = CharSet.Auto)]
|
||||||
internal static extern SafeFileHandle SetupDiGetClassDevs(
|
internal static extern SafeFileHandle SetupDiGetClassDevs(
|
||||||
ref Guid ClassGuid,
|
ref Guid ClassGuid,
|
||||||
|
|||||||
@@ -248,6 +248,7 @@ namespace DiscImageChef.Devices.Windows
|
|||||||
public short wIndex;
|
public short wIndex;
|
||||||
public short wLength;
|
public short wLength;
|
||||||
}
|
}
|
||||||
|
|
||||||
[StructLayout(LayoutKind.Sequential)]
|
[StructLayout(LayoutKind.Sequential)]
|
||||||
struct USB_DESCRIPTOR_REQUEST
|
struct USB_DESCRIPTOR_REQUEST
|
||||||
{
|
{
|
||||||
@@ -255,5 +256,34 @@ namespace DiscImageChef.Devices.Windows
|
|||||||
public USB_SETUP_PACKET SetupPacket;
|
public USB_SETUP_PACKET SetupPacket;
|
||||||
//public byte[] Data;
|
//public byte[] Data;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
[StructLayout(LayoutKind.Sequential)]
|
||||||
|
struct SffdiskQueryDeviceProtocolData
|
||||||
|
{
|
||||||
|
public ushort size;
|
||||||
|
public ushort reserved;
|
||||||
|
public Guid protocolGuid;
|
||||||
|
}
|
||||||
|
|
||||||
|
[StructLayout(LayoutKind.Sequential)]
|
||||||
|
struct SffdiskDeviceCommandData
|
||||||
|
{
|
||||||
|
public ushort size;
|
||||||
|
public ushort reserved;
|
||||||
|
public SffdiskDcmd command;
|
||||||
|
public ushort protocolArgumentSize;
|
||||||
|
public uint deviceDataBufferSize;
|
||||||
|
public uint information;
|
||||||
|
}
|
||||||
|
|
||||||
|
[StructLayout(LayoutKind.Sequential)]
|
||||||
|
struct SdCmdDescriptor
|
||||||
|
{
|
||||||
|
public byte commandCode;
|
||||||
|
public SdCommandClass cmdClass;
|
||||||
|
public SdTransferDirection transferDirection;
|
||||||
|
public SdTransferType transferType;
|
||||||
|
public SdResponseType responseType;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
Reference in New Issue
Block a user