Commit 14e34bed authored by Alexandre Julliard's avatar Alexandre Julliard

ntdll: Share dynamic exception table functions across platforms.

parent 44a60433
......@@ -6,7 +6,7 @@
@ cdecl -arch=arm,arm64,x86_64 RtlDeleteFunctionTable(ptr) ntdll.RtlDeleteFunctionTable
@ stdcall RtlFillMemory(ptr long long) ntdll.RtlFillMemory
@ cdecl -arch=arm,arm64,x86_64 RtlInstallFunctionTableCallback(long long long ptr ptr wstr) ntdll.RtlInstallFunctionTableCallback
@ stdcall -arch=arm,x86_64 RtlLookupFunctionEntry(long ptr ptr) ntdll.RtlLookupFunctionEntry
@ stdcall -arch=arm,arm64,x86_64 RtlLookupFunctionEntry(long ptr ptr) ntdll.RtlLookupFunctionEntry
@ stdcall RtlPcToFileHeader(ptr ptr) ntdll.RtlPcToFileHeader
@ stdcall -norelay RtlRaiseException(ptr) ntdll.RtlRaiseException
@ stdcall -arch=x86_64 RtlRestoreContext(ptr ptr) ntdll.RtlRestoreContext
......
......@@ -805,7 +805,7 @@
@ stdcall RtlLookupAtomInAtomTable(ptr wstr ptr)
@ stub RtlLookupElementGenericTable
# @ stub RtlLookupElementGenericTableAvl
@ stdcall -arch=arm,x86_64 RtlLookupFunctionEntry(long ptr ptr)
@ stdcall -arch=arm,arm64,x86_64 RtlLookupFunctionEntry(long ptr ptr)
@ stdcall RtlMakeSelfRelativeSD(ptr ptr ptr)
@ stdcall RtlMapGenericMask(ptr ptr)
# @ stub RtlMapSecurityErrorToNtStatus
......
......@@ -60,6 +60,10 @@ extern NTSTATUS set_thread_context( HANDLE handle, const context_t *context, BOO
extern NTSTATUS get_thread_context( HANDLE handle, context_t *context, unsigned int flags, BOOL *self ) DECLSPEC_HIDDEN;
extern LONG WINAPI call_unhandled_exception_filter( PEXCEPTION_POINTERS eptr ) DECLSPEC_HIDDEN;
#if defined(__x86_64__) || defined(__arm__) || defined(__aarch64__)
extern RUNTIME_FUNCTION *lookup_function_info( ULONG_PTR pc, ULONG_PTR *base, LDR_MODULE **module ) DECLSPEC_HIDDEN;
#endif
/* debug helpers */
extern LPCSTR debugstr_us( const UNICODE_STRING *str ) DECLSPEC_HIDDEN;
extern LPCSTR debugstr_ObjectAttributes(const OBJECT_ATTRIBUTES *oa) DECLSPEC_HIDDEN;
......
......@@ -128,15 +128,6 @@ typedef int (*wine_signal_handler)(unsigned int sig);
static wine_signal_handler handlers[256];
struct UNWIND_INFO
{
WORD function_length;
WORD unknown1 : 7;
WORD count : 5;
WORD unknown2 : 4;
};
/***********************************************************************
* get_trap_code
*
......@@ -1064,118 +1055,6 @@ void signal_init_process(void)
}
/**********************************************************************
* RtlAddFunctionTable (NTDLL.@)
*/
BOOLEAN CDECL RtlAddFunctionTable( RUNTIME_FUNCTION *table, DWORD count, DWORD addr )
{
FIXME( "%p %u %x: stub\n", table, count, addr );
return TRUE;
}
/**********************************************************************
* RtlInstallFunctionTableCallback (NTDLL.@)
*/
BOOLEAN CDECL RtlInstallFunctionTableCallback( DWORD table, DWORD base, DWORD length,
PGET_RUNTIME_FUNCTION_CALLBACK callback, PVOID context, PCWSTR dll )
{
FIXME( "%x %x %d %p %p %s: stub\n", table, base, length, callback, context, wine_dbgstr_w(dll) );
return TRUE;
}
/*************************************************************************
* RtlAddGrowableFunctionTable (NTDLL.@)
*/
DWORD WINAPI RtlAddGrowableFunctionTable( void **table, RUNTIME_FUNCTION *functions, DWORD count, DWORD max_count,
ULONG_PTR base, ULONG_PTR end )
{
FIXME( "(%p, %p, %d, %d, %ld, %ld) stub!\n", table, functions, count, max_count, base, end );
if (table) *table = NULL;
return STATUS_SUCCESS;
}
/*************************************************************************
* RtlGrowFunctionTable (NTDLL.@)
*/
void WINAPI RtlGrowFunctionTable( void *table, DWORD count )
{
FIXME( "(%p, %d) stub!\n", table, count );
}
/*************************************************************************
* RtlDeleteGrowableFunctionTable (NTDLL.@)
*/
void WINAPI RtlDeleteGrowableFunctionTable( void *table )
{
FIXME( "(%p) stub!\n", table );
}
/**********************************************************************
* RtlDeleteFunctionTable (NTDLL.@)
*/
BOOLEAN CDECL RtlDeleteFunctionTable( RUNTIME_FUNCTION *table )
{
FIXME( "%p: stub\n", table );
return TRUE;
}
/**********************************************************************
* find_function_info
*/
static RUNTIME_FUNCTION *find_function_info( ULONG_PTR pc, HMODULE module,
RUNTIME_FUNCTION *func, ULONG size )
{
int min = 0;
int max = size/sizeof(*func) - 1;
while (min <= max)
{
int pos = (min + max) / 2;
DWORD begin = (func[pos].BeginAddress & ~1), end;
if (func[pos].u.s.Flag)
end = begin + func[pos].u.s.FunctionLength * 2;
else
{
struct UNWIND_INFO *info;
info = (struct UNWIND_INFO *)((char *)module + func[pos].u.UnwindData);
end = begin + info->function_length * 2;
}
if ((char *)pc < (char *)module + begin) max = pos - 1;
else if ((char *)pc >= (char *)module + end) min = pos + 1;
else return func + pos;
}
return NULL;
}
/**********************************************************************
* RtlLookupFunctionEntry (NTDLL.@)
*/
PRUNTIME_FUNCTION WINAPI RtlLookupFunctionEntry( ULONG_PTR pc, DWORD *base,
UNWIND_HISTORY_TABLE *table )
{
LDR_MODULE *module;
RUNTIME_FUNCTION *func;
ULONG size;
/* FIXME: should use the history table to make things faster */
if (LdrFindEntryForAddress( (void *)pc, &module ))
{
WARN( "module not found for %lx\n", pc );
return NULL;
}
if (!(func = RtlImageDirectoryEntryToData( module->BaseAddress, TRUE,
IMAGE_DIRECTORY_ENTRY_EXCEPTION, &size )))
{
WARN( "no exception table found in module %p pc %lx\n", module->BaseAddress, pc );
return NULL;
}
func = find_function_info( pc, module->BaseAddress, func, size );
if (func) *base = (DWORD)module->BaseAddress;
return func;
}
/***********************************************************************
* RtlUnwind (NTDLL.@)
*/
......
......@@ -971,63 +971,6 @@ void signal_init_process(void)
}
/**********************************************************************
* RtlAddFunctionTable (NTDLL.@)
*/
BOOLEAN CDECL RtlAddFunctionTable( RUNTIME_FUNCTION *table, DWORD count, ULONG_PTR addr )
{
FIXME( "%p %u %lx: stub\n", table, count, addr );
return TRUE;
}
/**********************************************************************
* RtlInstallFunctionTableCallback (NTDLL.@)
*/
BOOLEAN CDECL RtlInstallFunctionTableCallback( ULONG_PTR table, ULONG_PTR base, DWORD length,
PGET_RUNTIME_FUNCTION_CALLBACK callback, PVOID context, PCWSTR dll )
{
FIXME( "%lx %lx %d %p %p %s: stub\n", table, base, length, callback, context, wine_dbgstr_w(dll) );
return TRUE;
}
/*************************************************************************
* RtlAddGrowableFunctionTable (NTDLL.@)
*/
DWORD WINAPI RtlAddGrowableFunctionTable( void **table, RUNTIME_FUNCTION *functions, DWORD count, DWORD max_count,
ULONG_PTR base, ULONG_PTR end )
{
FIXME( "(%p, %p, %d, %d, %ld, %ld) stub!\n", table, functions, count, max_count, base, end );
if (table) *table = NULL;
return STATUS_SUCCESS;
}
/*************************************************************************
* RtlGrowFunctionTable (NTDLL.@)
*/
void WINAPI RtlGrowFunctionTable( void *table, DWORD count )
{
FIXME( "(%p, %d) stub!\n", table, count );
}
/*************************************************************************
* RtlDeleteGrowableFunctionTable (NTDLL.@)
*/
void WINAPI RtlDeleteGrowableFunctionTable( void *table )
{
FIXME( "(%p) stub!\n", table );
}
/**********************************************************************
* RtlDeleteFunctionTable (NTDLL.@)
*/
BOOLEAN CDECL RtlDeleteFunctionTable( RUNTIME_FUNCTION *table )
{
FIXME( "%p: stub\n", table );
return TRUE;
}
/***********************************************************************
* RtlUnwind (NTDLL.@)
*/
......
......@@ -306,39 +306,6 @@ static inline struct amd64_thread_data *amd64_thread_data(void)
}
/***********************************************************************
* Dynamic unwind table
*/
struct dynamic_unwind_entry
{
struct list entry;
/* memory region which matches this entry */
DWORD64 base;
DWORD64 end;
/* lookup table */
RUNTIME_FUNCTION *table;
DWORD count;
DWORD max_count;
/* user defined callback */
PGET_RUNTIME_FUNCTION_CALLBACK callback;
PVOID context;
};
static struct list dynamic_unwind_list = LIST_INIT(dynamic_unwind_list);
static RTL_CRITICAL_SECTION dynamic_unwind_section;
static RTL_CRITICAL_SECTION_DEBUG dynamic_unwind_debug =
{
0, 0, &dynamic_unwind_section,
{ &dynamic_unwind_debug.ProcessLocksList, &dynamic_unwind_debug.ProcessLocksList },
0, 0, { (DWORD_PTR)(__FILE__ ": dynamic_unwind_section") }
};
static RTL_CRITICAL_SECTION dynamic_unwind_section = { &dynamic_unwind_debug, -1, 0, 0, 0, 0 };
/***********************************************************************
* Definitions for Win32 unwind tables
*/
......@@ -2501,76 +2468,6 @@ static inline CONTEXT *get_exception_context( EXCEPTION_RECORD *rec )
}
/**********************************************************************
* find_function_info
*/
static RUNTIME_FUNCTION *find_function_info( ULONG64 pc, HMODULE module,
RUNTIME_FUNCTION *func, ULONG size )
{
int min = 0;
int max = size - 1;
while (min <= max)
{
int pos = (min + max) / 2;
if ((char *)pc < (char *)module + func[pos].BeginAddress) max = pos - 1;
else if ((char *)pc >= (char *)module + func[pos].EndAddress) min = pos + 1;
else
{
func += pos;
while (func->UnwindData & 1) /* follow chained entry */
func = (RUNTIME_FUNCTION *)((char *)module + (func->UnwindData & ~1));
return func;
}
}
return NULL;
}
/**********************************************************************
* lookup_function_info
*/
static RUNTIME_FUNCTION *lookup_function_info( ULONG64 pc, ULONG64 *base, LDR_MODULE **module )
{
RUNTIME_FUNCTION *func = NULL;
struct dynamic_unwind_entry *entry;
ULONG size;
/* PE module or wine module */
if (!LdrFindEntryForAddress( (void *)pc, module ))
{
*base = (ULONG64)(*module)->BaseAddress;
if ((func = RtlImageDirectoryEntryToData( (*module)->BaseAddress, TRUE,
IMAGE_DIRECTORY_ENTRY_EXCEPTION, &size )))
{
/* lookup in function table */
func = find_function_info( pc, (*module)->BaseAddress, func, size/sizeof(*func) );
}
}
else
{
*module = NULL;
RtlEnterCriticalSection( &dynamic_unwind_section );
LIST_FOR_EACH_ENTRY( entry, &dynamic_unwind_list, struct dynamic_unwind_entry, entry )
{
if (pc >= entry->base && pc < entry->end)
{
*base = entry->base;
/* use callback or lookup in function table */
if (entry->callback)
func = entry->callback( pc, entry->context );
else
func = find_function_info( pc, (HMODULE)entry->base, entry->table, entry->count );
break;
}
}
RtlLeaveCriticalSection( &dynamic_unwind_section );
}
return func;
}
static DWORD __cdecl nested_exception_handler( EXCEPTION_RECORD *rec, EXCEPTION_REGISTRATION_RECORD *frame,
CONTEXT *context, EXCEPTION_REGISTRATION_RECORD **dispatcher )
{
......@@ -3436,205 +3333,6 @@ void signal_init_process(void)
}
/**********************************************************************
* RtlAddFunctionTable (NTDLL.@)
*/
BOOLEAN CDECL RtlAddFunctionTable( RUNTIME_FUNCTION *table, DWORD count, DWORD64 addr )
{
struct dynamic_unwind_entry *entry;
TRACE( "%p %u %lx\n", table, count, addr );
/* NOTE: Windows doesn't check if table is aligned or a NULL pointer */
entry = RtlAllocateHeap( GetProcessHeap(), 0, sizeof(*entry) );
if (!entry)
return FALSE;
entry->base = addr;
entry->end = addr + (count ? table[count - 1].EndAddress : 0);
entry->table = table;
entry->count = count;
entry->max_count = 0;
entry->callback = NULL;
entry->context = NULL;
RtlEnterCriticalSection( &dynamic_unwind_section );
list_add_tail( &dynamic_unwind_list, &entry->entry );
RtlLeaveCriticalSection( &dynamic_unwind_section );
return TRUE;
}
/**********************************************************************
* RtlInstallFunctionTableCallback (NTDLL.@)
*/
BOOLEAN CDECL RtlInstallFunctionTableCallback( DWORD64 table, DWORD64 base, DWORD length,
PGET_RUNTIME_FUNCTION_CALLBACK callback, PVOID context, PCWSTR dll )
{
struct dynamic_unwind_entry *entry;
TRACE( "%lx %lx %d %p %p %s\n", table, base, length, callback, context, wine_dbgstr_w(dll) );
/* NOTE: Windows doesn't check if the provided callback is a NULL pointer */
/* both low-order bits must be set */
if ((table & 0x3) != 0x3)
return FALSE;
entry = RtlAllocateHeap( GetProcessHeap(), 0, sizeof(*entry) );
if (!entry)
return FALSE;
entry->base = base;
entry->end = base + length;
entry->table = (RUNTIME_FUNCTION *)table;
entry->count = 0;
entry->max_count = 0;
entry->callback = callback;
entry->context = context;
RtlEnterCriticalSection( &dynamic_unwind_section );
list_add_tail( &dynamic_unwind_list, &entry->entry );
RtlLeaveCriticalSection( &dynamic_unwind_section );
return TRUE;
}
/*************************************************************************
* RtlAddGrowableFunctionTable (NTDLL.@)
*/
DWORD WINAPI RtlAddGrowableFunctionTable( void **table, RUNTIME_FUNCTION *functions, DWORD count, DWORD max_count,
ULONG_PTR base, ULONG_PTR end )
{
struct dynamic_unwind_entry *entry;
TRACE( "%p, %p, %u, %u, %lx, %lx\n", table, functions, count, max_count, base, end );
entry = RtlAllocateHeap( GetProcessHeap(), 0, sizeof(*entry) );
if (!entry)
return STATUS_NO_MEMORY;
entry->base = base;
entry->end = end;
entry->table = functions;
entry->count = count;
entry->max_count = max_count;
entry->callback = NULL;
entry->context = NULL;
RtlEnterCriticalSection( &dynamic_unwind_section );
list_add_tail( &dynamic_unwind_list, &entry->entry );
RtlLeaveCriticalSection( &dynamic_unwind_section );
*table = entry;
return STATUS_SUCCESS;
}
/*************************************************************************
* RtlGrowFunctionTable (NTDLL.@)
*/
void WINAPI RtlGrowFunctionTable( void *table, DWORD count )
{
struct dynamic_unwind_entry *entry;
TRACE( "%p, %u\n", table, count );
RtlEnterCriticalSection( &dynamic_unwind_section );
LIST_FOR_EACH_ENTRY( entry, &dynamic_unwind_list, struct dynamic_unwind_entry, entry )
{
if (entry == table)
{
if (count > entry->count && count <= entry->max_count)
entry->count = count;
break;
}
}
RtlLeaveCriticalSection( &dynamic_unwind_section );
}
/*************************************************************************
* RtlDeleteGrowableFunctionTable (NTDLL.@)
*/
void WINAPI RtlDeleteGrowableFunctionTable( void *table )
{
struct dynamic_unwind_entry *entry, *to_free = NULL;
TRACE( "%p\n", table );
RtlEnterCriticalSection( &dynamic_unwind_section );
LIST_FOR_EACH_ENTRY( entry, &dynamic_unwind_list, struct dynamic_unwind_entry, entry )
{
if (entry == table)
{
to_free = entry;
list_remove( &entry->entry );
break;
}
}
RtlLeaveCriticalSection( &dynamic_unwind_section );
RtlFreeHeap( GetProcessHeap(), 0, to_free );
}
/**********************************************************************
* RtlDeleteFunctionTable (NTDLL.@)
*/
BOOLEAN CDECL RtlDeleteFunctionTable( RUNTIME_FUNCTION *table )
{
struct dynamic_unwind_entry *entry, *to_free = NULL;
TRACE( "%p\n", table );
RtlEnterCriticalSection( &dynamic_unwind_section );
LIST_FOR_EACH_ENTRY( entry, &dynamic_unwind_list, struct dynamic_unwind_entry, entry )
{
if (entry->table == table)
{
to_free = entry;
list_remove( &entry->entry );
break;
}
}
RtlLeaveCriticalSection( &dynamic_unwind_section );
if (!to_free)
return FALSE;
RtlFreeHeap( GetProcessHeap(), 0, to_free );
return TRUE;
}
/**********************************************************************
* RtlLookupFunctionEntry (NTDLL.@)
*/
PRUNTIME_FUNCTION WINAPI RtlLookupFunctionEntry( ULONG64 pc, ULONG64 *base, UNWIND_HISTORY_TABLE *table )
{
LDR_MODULE *module;
RUNTIME_FUNCTION *func;
/* FIXME: should use the history table to make things faster */
func = lookup_function_info( pc, base, &module );
if (!func)
{
*base = 0;
if (module)
WARN( "no exception table found in module %p pc %lx\n", module->BaseAddress, pc );
else
WARN( "module not found for %lx\n", pc );
}
return func;
}
static ULONG64 get_int_reg( CONTEXT *context, int reg )
{
return *(&context->Rax + reg);
......
......@@ -1248,13 +1248,7 @@ typedef struct _DISPATCHER_CONTEXT
typedef LONG (CALLBACK *PEXCEPTION_FILTER)(struct _EXCEPTION_POINTERS*,PVOID);
typedef void (CALLBACK *PTERMINATION_HANDLER)(BOOLEAN,PVOID);
typedef PRUNTIME_FUNCTION (CALLBACK *PGET_RUNTIME_FUNCTION_CALLBACK)(DWORD64,PVOID);
BOOLEAN CDECL RtlAddFunctionTable(RUNTIME_FUNCTION*,DWORD,DWORD64);
BOOLEAN CDECL RtlDeleteFunctionTable(RUNTIME_FUNCTION*);
BOOLEAN CDECL RtlInstallFunctionTableCallback(DWORD64,DWORD64,DWORD,PGET_RUNTIME_FUNCTION_CALLBACK,PVOID,PCWSTR);
PRUNTIME_FUNCTION WINAPI RtlLookupFunctionEntry(DWORD64,DWORD64*,UNWIND_HISTORY_TABLE*);
PVOID WINAPI RtlVirtualUnwind(ULONG,ULONG64,ULONG64,RUNTIME_FUNCTION*,CONTEXT*,PVOID*,ULONG64*,KNONVOLATILE_CONTEXT_POINTERS*);
PVOID WINAPI RtlVirtualUnwind(ULONG,ULONG64,ULONG64,RUNTIME_FUNCTION*,CONTEXT*,PVOID*,ULONG64*,KNONVOLATILE_CONTEXT_POINTERS*);
#define UNW_FLAG_NHANDLER 0
#define UNW_FLAG_EHANDLER 1
......@@ -1821,12 +1815,11 @@ typedef struct _DISPATCHER_CONTEXT
typedef LONG (CALLBACK *PEXCEPTION_FILTER)(struct _EXCEPTION_POINTERS*,DWORD);
typedef void (CALLBACK *PTERMINATION_HANDLER)(BOOLEAN,DWORD);
typedef PRUNTIME_FUNCTION (CALLBACK *PGET_RUNTIME_FUNCTION_CALLBACK)(DWORD,PVOID);
PVOID WINAPI RtlVirtualUnwind(DWORD,DWORD,DWORD,RUNTIME_FUNCTION*,CONTEXT*,PVOID*,DWORD*,KNONVOLATILE_CONTEXT_POINTERS*);
BOOLEAN CDECL RtlAddFunctionTable(RUNTIME_FUNCTION*,DWORD,DWORD);
BOOLEAN CDECL RtlDeleteFunctionTable(RUNTIME_FUNCTION*);
BOOLEAN CDECL RtlInstallFunctionTableCallback(DWORD,DWORD,DWORD,PGET_RUNTIME_FUNCTION_CALLBACK,PVOID,PCWSTR);
PRUNTIME_FUNCTION WINAPI RtlLookupFunctionEntry(ULONG_PTR,DWORD*,UNWIND_HISTORY_TABLE*);
#define UNW_FLAG_NHANDLER 0
#define UNW_FLAG_EHANDLER 1
#define UNW_FLAG_UHANDLER 2
#endif /* __arm__ */
......@@ -2000,11 +1993,11 @@ typedef struct _DISPATCHER_CONTEXT
typedef LONG (CALLBACK *PEXCEPTION_FILTER)(struct _EXCEPTION_POINTERS*,DWORD64);
typedef void (CALLBACK *PTERMINATION_HANDLER)(BOOLEAN,DWORD64);
typedef PRUNTIME_FUNCTION (CALLBACK *PGET_RUNTIME_FUNCTION_CALLBACK)(DWORD64,PVOID);
PVOID WINAPI RtlVirtualUnwind(DWORD,ULONG_PTR,ULONG_PTR,RUNTIME_FUNCTION*,CONTEXT*,PVOID*,ULONG_PTR*,KNONVOLATILE_CONTEXT_POINTERS*);
BOOLEAN CDECL RtlAddFunctionTable(RUNTIME_FUNCTION*,DWORD,ULONG_PTR);
BOOLEAN CDECL RtlDeleteFunctionTable(RUNTIME_FUNCTION*);
BOOLEAN CDECL RtlInstallFunctionTableCallback(ULONG_PTR,ULONG_PTR,DWORD,PGET_RUNTIME_FUNCTION_CALLBACK,PVOID,PCWSTR);
#define UNW_FLAG_NHANDLER 0
#define UNW_FLAG_EHANDLER 1
#define UNW_FLAG_UHANDLER 2
#endif /* __aarch64__ */
......@@ -2315,6 +2308,19 @@ typedef struct _WOW64_CONTEXT
} WOW64_CONTEXT, *PWOW64_CONTEXT;
#include "poppack.h"
#if defined(__x86_64__) || defined(__arm__) || defined(__aarch64__)
typedef PRUNTIME_FUNCTION (CALLBACK *PGET_RUNTIME_FUNCTION_CALLBACK)(DWORD_PTR,PVOID);
BOOLEAN CDECL RtlAddFunctionTable(RUNTIME_FUNCTION*,DWORD,DWORD_PTR);
DWORD WINAPI RtlAddGrowableFunctionTable(void**,PRUNTIME_FUNCTION,DWORD,DWORD,ULONG_PTR,ULONG_PTR);
BOOLEAN CDECL RtlDeleteFunctionTable(RUNTIME_FUNCTION*);
void WINAPI RtlDeleteGrowableFunctionTable(void*);
void WINAPI RtlGrowFunctionTable(void*,DWORD);
BOOLEAN CDECL RtlInstallFunctionTableCallback(DWORD_PTR,DWORD_PTR,DWORD,PGET_RUNTIME_FUNCTION_CALLBACK,PVOID,PCWSTR);
PRUNTIME_FUNCTION WINAPI RtlLookupFunctionEntry(DWORD_PTR,DWORD_PTR*,UNWIND_HISTORY_TABLE*);
void WINAPI RtlUnwindEx(PVOID,PVOID,struct _EXCEPTION_RECORD*,PVOID,CONTEXT*,UNWIND_HISTORY_TABLE*);
#endif
/*
* Product types
......
Markdown is supported
0% or
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment