Jim Park's Unicode NSIS merging - Step 1 : switch to TCHARs where relevant.
Compiler output is identical before & after this step git-svn-id: https://svn.code.sf.net/p/nsis/code/NSIS/branches/wizou@6036 212acab6-be3b-0410-9dea-997c60f758d6
This commit is contained in:
parent
4e48722b63
commit
752d7d239a
209 changed files with 9698 additions and 7658 deletions
|
@ -17,6 +17,8 @@ freely, subject to the following restrictions:
|
|||
misrepresented as being the original software.
|
||||
3. This notice may not be removed or altered from any source distribution.
|
||||
|
||||
Unicode support by Jim Park -- 08/24/2007
|
||||
|
||||
*/
|
||||
#include <windows.h>
|
||||
#include <commctrl.h>
|
||||
|
@ -35,11 +37,12 @@ HWND g_hwndParent;
|
|||
HWND g_hwndList;
|
||||
|
||||
void ExecScript(BOOL log);
|
||||
void LogMessage(const char *pStr, BOOL bOEM);
|
||||
char *my_strstr(char *a, char *b);
|
||||
unsigned int my_atoi(char *s);
|
||||
void LogMessage(const TCHAR *pStr, BOOL bOEM);
|
||||
TCHAR *my_strstr(TCHAR *a, TCHAR *b);
|
||||
unsigned int my_atoi(TCHAR *s);
|
||||
int WINAPI _tWinMain(HINSTANCE hInstance, HINSTANCE hPrevInstance, LPTSTR lpCmdLine, int nCmdShow);
|
||||
|
||||
void __declspec(dllexport) Exec(HWND hwndParent, int string_size, char *variables, stack_t **stacktop) {
|
||||
void __declspec(dllexport) Exec(HWND hwndParent, int string_size, TCHAR *variables, stack_t **stacktop) {
|
||||
g_hwndParent=hwndParent;
|
||||
EXDLL_INIT();
|
||||
{
|
||||
|
@ -47,7 +50,7 @@ void __declspec(dllexport) Exec(HWND hwndParent, int string_size, char *variable
|
|||
}
|
||||
}
|
||||
|
||||
void __declspec(dllexport) ExecToLog(HWND hwndParent, int string_size, char *variables, stack_t **stacktop) {
|
||||
void __declspec(dllexport) ExecToLog(HWND hwndParent, int string_size, TCHAR *variables, stack_t **stacktop) {
|
||||
g_hwndParent=hwndParent;
|
||||
EXDLL_INIT();
|
||||
{
|
||||
|
@ -55,7 +58,7 @@ void __declspec(dllexport) ExecToLog(HWND hwndParent, int string_size, char *var
|
|||
}
|
||||
}
|
||||
|
||||
void __declspec(dllexport) ExecToStack(HWND hwndParent, int string_size, char *variables, stack_t **stacktop) {
|
||||
void __declspec(dllexport) ExecToStack(HWND hwndParent, int string_size, TCHAR *variables, stack_t **stacktop) {
|
||||
g_hwndParent=hwndParent;
|
||||
EXDLL_INIT();
|
||||
{
|
||||
|
@ -69,7 +72,7 @@ BOOL WINAPI DllMain(HANDLE hInst, ULONG ul_reason_for_call, LPVOID lpReserved) {
|
|||
return TRUE;
|
||||
}
|
||||
|
||||
#define TAB_REPLACE " "
|
||||
#define TAB_REPLACE _T(" ")
|
||||
#define TAB_REPLACE_SIZE (sizeof(TAB_REPLACE)-1)
|
||||
|
||||
// Turn a pair of chars into a word
|
||||
|
@ -88,7 +91,7 @@ BOOL IsWOW64() {
|
|||
LPFN_ISWOW64PROCESS fnIsWow64Process;
|
||||
|
||||
fnIsWow64Process = (LPFN_ISWOW64PROCESS) GetProcAddress(
|
||||
GetModuleHandle("kernel32"), "IsWow64Process");
|
||||
GetModuleHandle(_T("kernel32")), "IsWow64Process");
|
||||
|
||||
if (fnIsWow64Process != NULL) {
|
||||
if (fnIsWow64Process(GetCurrentProcess(), &wow64)) {
|
||||
|
@ -100,27 +103,28 @@ BOOL IsWOW64() {
|
|||
}
|
||||
|
||||
void ExecScript(int log) {
|
||||
char szRet[128] = "";
|
||||
char meDLLPath[MAX_PATH];
|
||||
char *executor;
|
||||
char *g_exec;
|
||||
char *pExec;
|
||||
TCHAR szRet[128] = _T("");
|
||||
TCHAR meDLLPath[MAX_PATH];
|
||||
TCHAR *executor;
|
||||
TCHAR *g_exec;
|
||||
TCHAR *pExec;
|
||||
unsigned int g_to;
|
||||
BOOL bOEM;
|
||||
|
||||
if (!IsWOW64()) {
|
||||
char *p;
|
||||
TCHAR* p;
|
||||
int nComSpecSize;
|
||||
|
||||
nComSpecSize = GetModuleFileName(g_hInst, meDLLPath, MAX_PATH) + 2; // 2 chars for quotes
|
||||
g_exec = (char *)GlobalAlloc(GPTR, sizeof(char)*g_stringsize+nComSpecSize+2); // 1 for space, 1 for null
|
||||
g_exec = (TCHAR *)GlobalAlloc(GPTR, sizeof(TCHAR)*(g_stringsize+nComSpecSize+2)); // 1 for space, 1 for null
|
||||
p = meDLLPath + nComSpecSize - 2; // point p at null char of meDLLPath
|
||||
*g_exec = '"';
|
||||
*g_exec = _T('"');
|
||||
executor = g_exec + 1;
|
||||
|
||||
// Look for the last '\' in path.
|
||||
do
|
||||
{
|
||||
if (*p == '\\')
|
||||
if (*p == _T('\\'))
|
||||
break;
|
||||
p = CharPrev(meDLLPath, p);
|
||||
}
|
||||
|
@ -128,15 +132,15 @@ void ExecScript(int log) {
|
|||
if (p == meDLLPath)
|
||||
{
|
||||
// bad path
|
||||
pushstring("error");
|
||||
pushstring(_T("error"));
|
||||
GlobalFree(g_exec);
|
||||
return;
|
||||
}
|
||||
|
||||
*p = 0;
|
||||
GetTempFileName(meDLLPath, "ns", 0, executor);
|
||||
*p = '\\';
|
||||
if (CopyFile(meDLLPath, executor, FALSE))
|
||||
GetTempFileName(meDLLPath, _T("ns"), 0, executor); // executor = new temp file name in module path.
|
||||
*p = _T('\\');
|
||||
if (CopyFile(meDLLPath, executor, FALSE)) // copy current DLL to temp file in module path.
|
||||
{
|
||||
HANDLE hFile, hMapping;
|
||||
LPBYTE pMapView;
|
||||
|
@ -147,25 +151,27 @@ void ExecScript(int log) {
|
|||
if (pMapView)
|
||||
{
|
||||
pNTHeaders = (PIMAGE_NT_HEADERS)(pMapView + ((PIMAGE_DOS_HEADER)pMapView)->e_lfanew);
|
||||
// Turning the copied DLL into a stripped down executable.
|
||||
pNTHeaders->FileHeader.Characteristics = IMAGE_FILE_32BIT_MACHINE | IMAGE_FILE_LOCAL_SYMS_STRIPPED |
|
||||
IMAGE_FILE_LINE_NUMS_STRIPPED | IMAGE_FILE_EXECUTABLE_IMAGE;
|
||||
// Windows character-mode user interface (CUI) subsystem.
|
||||
pNTHeaders->OptionalHeader.Subsystem = IMAGE_SUBSYSTEM_WINDOWS_CUI;
|
||||
pNTHeaders->OptionalHeader.AddressOfEntryPoint = (DWORD)WinMain - (DWORD)g_hInst;
|
||||
pNTHeaders->OptionalHeader.AddressOfEntryPoint = (DWORD)_tWinMain - (DWORD)g_hInst;
|
||||
UnmapViewOfFile(pMapView);
|
||||
}
|
||||
CloseHandle(hMapping);
|
||||
CloseHandle(hFile);
|
||||
}
|
||||
|
||||
lstrcat(g_exec, "\"");
|
||||
lstrcat(g_exec, _T("\""));
|
||||
|
||||
// add space
|
||||
pExec = g_exec + lstrlen(g_exec);
|
||||
*pExec = ' ';
|
||||
*pExec = _T(' ');
|
||||
pExec++;
|
||||
} else {
|
||||
executor = NULL;
|
||||
g_exec = (char *)GlobalAlloc(GPTR, sizeof(char)*g_stringsize+1); // 1 for null
|
||||
g_exec = (TCHAR *)GlobalAlloc(GPTR, sizeof(TCHAR)*(g_stringsize+1)); // 1 for NULL
|
||||
pExec = g_exec;
|
||||
}
|
||||
|
||||
|
@ -173,18 +179,24 @@ void ExecScript(int log) {
|
|||
bOEM = FALSE; // default is no OEM->ANSI conversion
|
||||
|
||||
g_hwndList = NULL;
|
||||
if (g_hwndParent)
|
||||
g_hwndList = FindWindowEx(FindWindowEx(g_hwndParent,NULL,"#32770",NULL),NULL,"SysListView32",NULL);
|
||||
|
||||
// g_hwndParent = the caller, usually NSIS installer.
|
||||
if (g_hwndParent) // The window class name for dialog boxes is "#32770"
|
||||
g_hwndList = FindWindowEx(FindWindowEx(g_hwndParent,NULL,_T("#32770"),NULL),NULL,_T("SysListView32"),NULL);
|
||||
|
||||
// g_exec is the complete command to run: It has the copy of this DLL turned
|
||||
// into an executable right now.
|
||||
|
||||
params:
|
||||
// Get the command I need to run from the NSIS stack.
|
||||
popstring(pExec);
|
||||
if (my_strstr(pExec, "/TIMEOUT=") == pExec) {
|
||||
char *szTimeout = pExec + 9;
|
||||
if (my_strstr(pExec, _T("/TIMEOUT=")) == pExec) {
|
||||
TCHAR *szTimeout = pExec + 9;
|
||||
g_to = my_atoi(szTimeout);
|
||||
*pExec = 0;
|
||||
goto params;
|
||||
}
|
||||
if (!lstrcmpi(pExec, "/OEM")) {
|
||||
if (!lstrcmpi(pExec, _T("/OEM"))) {
|
||||
bOEM = TRUE;
|
||||
*pExec = 0;
|
||||
goto params;
|
||||
|
@ -192,12 +204,14 @@ params:
|
|||
|
||||
if (!pExec[0])
|
||||
{
|
||||
pushstring("error");
|
||||
*(pExec-2) = '\0'; // skip space and quote
|
||||
pushstring(_T("error"));
|
||||
*(pExec-2) = _T('\0'); // skip space and quote
|
||||
if (executor) DeleteFile(executor);
|
||||
GlobalFree(g_exec);
|
||||
return;
|
||||
}
|
||||
|
||||
// Got all the params off the stack.
|
||||
|
||||
{
|
||||
STARTUPINFO si={sizeof(si),};
|
||||
|
@ -211,20 +225,20 @@ params:
|
|||
DWORD dwExit = 0;
|
||||
DWORD dwWait = WAIT_TIMEOUT;
|
||||
DWORD dwLastOutput;
|
||||
static char szBuf[1024];
|
||||
static TCHAR szBuf[1024];
|
||||
HGLOBAL hUnusedBuf = NULL;
|
||||
char *szUnusedBuf = 0;
|
||||
TCHAR *szUnusedBuf = 0;
|
||||
|
||||
if (log) {
|
||||
hUnusedBuf = GlobalAlloc(GHND, log & 2 ? g_stringsize : sizeof(szBuf)*4);
|
||||
hUnusedBuf = GlobalAlloc(GHND, log & 2 ? (g_stringsize*sizeof(TCHAR)) : sizeof(szBuf)*4);
|
||||
if (!hUnusedBuf) {
|
||||
lstrcpy(szRet, "error");
|
||||
lstrcpy(szRet, _T("error"));
|
||||
goto done;
|
||||
}
|
||||
szUnusedBuf = (char *)GlobalLock(hUnusedBuf);
|
||||
szUnusedBuf = (TCHAR *)GlobalLock(hUnusedBuf);
|
||||
}
|
||||
|
||||
GetVersionEx(&osv);
|
||||
GetVersionEx(&osv); // Get OS info
|
||||
if (osv.dwPlatformId == VER_PLATFORM_WIN32_NT) {
|
||||
InitializeSecurityDescriptor(&sd,SECURITY_DESCRIPTOR_REVISION);
|
||||
SetSecurityDescriptorDacl(&sd,true,NULL,false);
|
||||
|
@ -234,11 +248,11 @@ params:
|
|||
sa.lpSecurityDescriptor = NULL;
|
||||
sa.bInheritHandle = true;
|
||||
if (!CreatePipe(&read_stdout,&newstdout,&sa,0)) {
|
||||
lstrcpy(szRet, "error");
|
||||
lstrcpy(szRet, _T("error"));
|
||||
goto done;
|
||||
}
|
||||
if (!CreatePipe(&read_stdin,&newstdin,&sa,0)) {
|
||||
lstrcpy(szRet, "error");
|
||||
lstrcpy(szRet, _T("error"));
|
||||
goto done;
|
||||
}
|
||||
|
||||
|
@ -249,12 +263,13 @@ params:
|
|||
si.hStdOutput = newstdout;
|
||||
si.hStdError = newstdout;
|
||||
if (!CreateProcess(NULL,g_exec,NULL,NULL,TRUE,CREATE_NEW_CONSOLE,NULL,NULL,&si,&pi)) {
|
||||
lstrcpy(szRet, "error");
|
||||
lstrcpy(szRet, _T("error"));
|
||||
goto done;
|
||||
}
|
||||
|
||||
dwLastOutput = GetTickCount();
|
||||
|
||||
// Now I'm talking with an executable copy of myself.
|
||||
while (dwWait != WAIT_OBJECT_0 || dwRead) {
|
||||
PeekNamedPipe(read_stdout, 0, 0, 0, &dwRead, NULL);
|
||||
if (dwRead) {
|
||||
|
@ -262,16 +277,16 @@ params:
|
|||
ReadFile(read_stdout, szBuf, sizeof(szBuf)-1, &dwRead, NULL);
|
||||
szBuf[dwRead] = 0;
|
||||
if (log) {
|
||||
char *p, *p2;
|
||||
TCHAR *p, *p2;
|
||||
SIZE_T iReqLen = lstrlen(szBuf) + lstrlen(szUnusedBuf);
|
||||
if (GlobalSize(hUnusedBuf) < iReqLen && (iReqLen < g_stringsize || !(log & 2))) {
|
||||
GlobalUnlock(hUnusedBuf);
|
||||
hUnusedBuf = GlobalReAlloc(hUnusedBuf, iReqLen+sizeof(szBuf), GHND);
|
||||
if (!hUnusedBuf) {
|
||||
lstrcpy(szRet, "error");
|
||||
lstrcpy(szRet, _T("error"));
|
||||
break;
|
||||
}
|
||||
szUnusedBuf = (char *)GlobalLock(hUnusedBuf);
|
||||
szUnusedBuf = (TCHAR *)GlobalLock(hUnusedBuf);
|
||||
}
|
||||
p = szUnusedBuf; // get the old left overs
|
||||
if (iReqLen < g_stringsize || !(log & 2)) lstrcat(p, szBuf);
|
||||
|
@ -280,33 +295,33 @@ params:
|
|||
}
|
||||
|
||||
if (!(log & 2)) {
|
||||
while ((p = my_strstr(p, "\t"))) {
|
||||
while ((p = my_strstr(p, _T("\t")))) {
|
||||
if ((int)(p - szUnusedBuf) > (int)(GlobalSize(hUnusedBuf) - TAB_REPLACE_SIZE - 1))
|
||||
{
|
||||
*p++ = ' ';
|
||||
*p++ = _T(' ');
|
||||
}
|
||||
else
|
||||
{
|
||||
int len = lstrlen(p);
|
||||
char *c_out=(char*)p+TAB_REPLACE_SIZE+len;
|
||||
char *c_in=(char *)p+len;
|
||||
TCHAR *c_out=(TCHAR*)p+TAB_REPLACE_SIZE+len;
|
||||
TCHAR *c_in=(TCHAR *)p+len;
|
||||
while (len-- > 0) {
|
||||
*c_out--=*c_in--;
|
||||
}
|
||||
|
||||
lstrcpy(p, TAB_REPLACE);
|
||||
p += TAB_REPLACE_SIZE;
|
||||
*p = ' ';
|
||||
*p = _T(' ');
|
||||
}
|
||||
}
|
||||
|
||||
p = szUnusedBuf; // get the old left overs
|
||||
for (p2 = p; *p2;) {
|
||||
if (*p2 == '\r') {
|
||||
if (*p2 == _T('\r')) {
|
||||
*p2++ = 0;
|
||||
continue;
|
||||
}
|
||||
if (*p2 == '\n') {
|
||||
if (*p2 == _T('\n')) {
|
||||
*p2 = 0;
|
||||
while (!*p && p != p2) p++;
|
||||
LogMessage(p, bOEM);
|
||||
|
@ -318,7 +333,7 @@ params:
|
|||
|
||||
// If data was taken out from the unused buffer, move p contents to the start of szUnusedBuf
|
||||
if (p != szUnusedBuf) {
|
||||
char *p2 = szUnusedBuf;
|
||||
TCHAR *p2 = szUnusedBuf;
|
||||
while (*p) *p2++ = *p++;
|
||||
*p2 = 0;
|
||||
}
|
||||
|
@ -328,7 +343,7 @@ params:
|
|||
else {
|
||||
if (g_to && GetTickCount() > dwLastOutput+g_to) {
|
||||
TerminateProcess(pi.hProcess, -1);
|
||||
lstrcpy(szRet, "timeout");
|
||||
lstrcpy(szRet, _T("timeout"));
|
||||
}
|
||||
else Sleep(LOOPTIMEOUT);
|
||||
}
|
||||
|
@ -341,8 +356,8 @@ done:
|
|||
if (log & 2) pushstring(szUnusedBuf);
|
||||
if (log & 1 && *szUnusedBuf) LogMessage(szUnusedBuf, bOEM);
|
||||
if ( dwExit == STATUS_ILLEGAL_INSTRUCTION )
|
||||
lstrcpy(szRet, "error");
|
||||
if (!szRet[0]) wsprintf(szRet,"%d",dwExit);
|
||||
lstrcpy(szRet, _T("error"));
|
||||
if (!szRet[0]) wsprintf(szRet,_T("%d"),dwExit);
|
||||
pushstring(szRet);
|
||||
CloseHandle(pi.hThread);
|
||||
CloseHandle(pi.hProcess);
|
||||
|
@ -350,7 +365,7 @@ done:
|
|||
CloseHandle(read_stdout);
|
||||
CloseHandle(newstdin);
|
||||
CloseHandle(read_stdin);
|
||||
*(pExec-2) = '\0'; // skip space and quote
|
||||
*(pExec-2) = _T('\0'); // skip space and quote
|
||||
if (executor) DeleteFile(executor);
|
||||
GlobalFree(g_exec);
|
||||
if (log) {
|
||||
|
@ -361,27 +376,27 @@ done:
|
|||
}
|
||||
|
||||
// Tim Kosse's LogMessage
|
||||
void LogMessage(const char *pStr, BOOL bOEM) {
|
||||
void LogMessage(const TCHAR *pStr, BOOL bOEM) {
|
||||
LVITEM item={0};
|
||||
int nItemCount;
|
||||
if (!g_hwndList) return;
|
||||
//if (!lstrlen(pStr)) return;
|
||||
if (bOEM == TRUE) OemToCharBuff(pStr, (char *)pStr, lstrlen(pStr));
|
||||
if (bOEM == TRUE) OemToCharBuff(pStr, (TCHAR *)pStr, lstrlen(pStr));
|
||||
nItemCount=SendMessage(g_hwndList, LVM_GETITEMCOUNT, 0, 0);
|
||||
item.mask=LVIF_TEXT;
|
||||
item.pszText=(char *)pStr;
|
||||
item.pszText=(TCHAR *)pStr;
|
||||
item.cchTextMax=0;
|
||||
item.iItem=nItemCount;
|
||||
ListView_InsertItem(g_hwndList, &item);
|
||||
ListView_EnsureVisible(g_hwndList, item.iItem, 0);
|
||||
}
|
||||
|
||||
char *my_strstr(char *a, char *b)
|
||||
TCHAR *my_strstr(TCHAR *a, TCHAR *b)
|
||||
{
|
||||
int l = lstrlen(b);
|
||||
while (lstrlen(a) >= l)
|
||||
{
|
||||
char c = a[l];
|
||||
TCHAR c = a[l];
|
||||
a[l] = 0;
|
||||
if (!lstrcmpi(a, b))
|
||||
{
|
||||
|
@ -394,25 +409,25 @@ char *my_strstr(char *a, char *b)
|
|||
return NULL;
|
||||
}
|
||||
|
||||
unsigned int my_atoi(char *s) {
|
||||
unsigned int my_atoi(TCHAR *s) {
|
||||
unsigned int v=0;
|
||||
if (*s == '0' && (s[1] == 'x' || s[1] == 'X')) {
|
||||
if (*s == _T('0') && (s[1] == _T('x') || s[1] == _T('X'))) {
|
||||
s+=2;
|
||||
for (;;) {
|
||||
int c=*s++;
|
||||
if (c >= '0' && c <= '9') c-='0';
|
||||
else if (c >= 'a' && c <= 'f') c-='a'-10;
|
||||
else if (c >= 'A' && c <= 'F') c-='A'-10;
|
||||
if (c >= _T('0') && c <= _T('9')) c-=_T('0');
|
||||
else if (c >= _T('a') && c <= _T('f')) c-=_T('a')-10;
|
||||
else if (c >= _T('A') && c <= _T('F')) c-=_T('A')-10;
|
||||
else break;
|
||||
v<<=4;
|
||||
v+=c;
|
||||
}
|
||||
}
|
||||
else if (*s == '0' && s[1] <= '7' && s[1] >= '0') {
|
||||
else if (*s == _T('0') && s[1] <= _T('7') && s[1] >= _T('0')) {
|
||||
s++;
|
||||
for (;;) {
|
||||
int c=*s++;
|
||||
if (c >= '0' && c <= '7') c-='0';
|
||||
if (c >= _T('0') && c <= _T('7')) c-=_T('0');
|
||||
else break;
|
||||
v<<=3;
|
||||
v+=c;
|
||||
|
@ -420,7 +435,7 @@ unsigned int my_atoi(char *s) {
|
|||
}
|
||||
else {
|
||||
for (;;) {
|
||||
int c=*s++ - '0';
|
||||
int c=*s++ - _T('0');
|
||||
if (c < 0 || c > 9) break;
|
||||
v*=10;
|
||||
v+=c;
|
||||
|
@ -429,14 +444,14 @@ unsigned int my_atoi(char *s) {
|
|||
return (int)v;
|
||||
}
|
||||
|
||||
int WINAPI WinMain(HINSTANCE hInstance, HINSTANCE hPrevInstance, LPSTR lpCmdLine, int nCmdShow)
|
||||
int WINAPI _tWinMain(HINSTANCE hInstance, HINSTANCE hPrevInstance, LPTSTR lpCmdLine, int nCmdShow)
|
||||
{
|
||||
DWORD Ret;
|
||||
STARTUPINFO si = {0};
|
||||
PROCESS_INFORMATION pi = {0};
|
||||
char command_line[1024];
|
||||
char seekchar=' ';
|
||||
char *cmdline;
|
||||
TCHAR command_line[1024];
|
||||
TCHAR seekchar=_T(' ');
|
||||
TCHAR *cmdline;
|
||||
|
||||
si.cb = sizeof(si);
|
||||
// Make child process use this app's standard files. Not needed because the handles
|
||||
|
@ -448,12 +463,12 @@ int WINAPI WinMain(HINSTANCE hInstance, HINSTANCE hPrevInstance, LPSTR lpCmdLine
|
|||
lstrcpyn(command_line, GetCommandLine(), 1024);
|
||||
|
||||
cmdline = command_line;
|
||||
if (*cmdline == '\"') seekchar = *cmdline++;
|
||||
if (*cmdline == _T('\"')) seekchar = *cmdline++;
|
||||
|
||||
while (*cmdline && *cmdline != seekchar) cmdline=CharNext(cmdline);
|
||||
cmdline=CharNext(cmdline);
|
||||
// skip any spaces before the arguments
|
||||
while (*cmdline && *cmdline == ' ') cmdline++;
|
||||
while (*cmdline && *cmdline == _T(' ')) cmdline++;
|
||||
|
||||
Ret = CreateProcess (NULL, cmdline,
|
||||
NULL, NULL,
|
||||
|
|
Loading…
Add table
Add a link
Reference in a new issue