Skip to content

Fix passing of non-ASCII characters as command-line arguments #127

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Open
wants to merge 3 commits into
base: main
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
8 changes: 4 additions & 4 deletions Base/Testing/Cpp/ctkAppLauncherEnvironmentTest.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -52,24 +52,24 @@ void ctkAppLauncherEnvironmentTester::cleanup()
QStringList originalEnvKeys = ctkAppLauncherEnvironment::envKeys(this->OriginalEnv);
foreach(const QString& varName, originalEnvKeys)
{
qputenv(varName.toLatin1(), this->OriginalEnv.value(varName).toLatin1());
qputenv(varName.toLocal8Bit(), this->OriginalEnv.value(varName).toLocal8Bit());
}
}

// ----------------------------------------------------------------------------
void ctkAppLauncherEnvironmentTester::setEnv(const QString& name, const QString& value)
{
qputenv(name.toLatin1(), value.toLatin1());
qputenv(name.toLocal8Bit(), value.toLocal8Bit());
this->VariableNames.insert(name);
}

// ----------------------------------------------------------------------------
void ctkAppLauncherEnvironmentTester::unsetEnv(const QString& name)
{
#if defined(_MSC_VER)
qputenv(name.toLatin1(), QString("").toLatin1());
qputenv(name.toLocal8Bit(), QString().toLocal8Bit());
#else
unsetenv(name.toLatin1());
unsetenv(name.toLocal8Bit());
#endif
}

Expand Down
2 changes: 1 addition & 1 deletion Base/ctkAppArguments.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -58,7 +58,7 @@ void ctkChar2DArray::setValues(const QStringList& list)
{
QString item = d->List.at(index);
d->Values[index] = new char[item.size() + 1];
qstrcpy(d->Values[index], item.toLatin1().data());
qstrcpy(d->Values[index], item.toLocal8Bit().data());
}
}

Expand Down
6 changes: 3 additions & 3 deletions Base/ctkAppLauncherEnvironment.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -166,9 +166,9 @@ void ctkAppLauncherEnvironment::updateCurrentEnvironment(const QProcessEnvironme
foreach(const QString& varName, variablesToUnset)
{
#if defined(Q_OS_WIN32)
bool success = qputenv(varName.toLatin1(), QString("").toLatin1());
bool success = qputenv(varName.toLocal8Bit(), QString().toLocal8Bit());
#else
bool success = unsetenv(varName.toLatin1()) == EXIT_SUCCESS;
bool success = unsetenv(varName.toLocal8Bit()) == EXIT_SUCCESS;
#endif
if (!success)
{
Expand All @@ -180,7 +180,7 @@ void ctkAppLauncherEnvironment::updateCurrentEnvironment(const QProcessEnvironme
foreach(const QString& varName, envKeys)
{
QString varValue = environment.value(varName);
bool success = qputenv(varName.toLatin1(), varValue.toLatin1());
bool success = qputenv(varName.toLocal8Bit(), varValue.toLocal8Bit());
if (!success)
{
qWarning() << "Failed to set environment variable"
Expand Down
138 changes: 51 additions & 87 deletions Base/ctkCommandLineParser.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -819,106 +819,70 @@ void ctkCommandLineParser::setStrictModeEnabled(bool strictMode)
this->Internal->StrictMode = strictMode;
}

#if defined (_WIN32)
// --------------------------------------------------------------------------
void ctkCommandLineParser::convertWindowsCommandLineToUnixArguments(
const char *cmd_line, int *argc, char ***argv)
PWSTR cmd_line, int* argc, char*** argv)
{
if (!cmd_line || !argc || !argv)
{
return;
}

// A space delimites an argument except when it is inside a quote

(*argc) = 1;

size_t cmd_line_len = strlen(cmd_line);

size_t i;
for (i = 0; i < cmd_line_len; i++)
{
while (isspace(cmd_line[i]) && i < cmd_line_len)
{
i++;
}
if (i < cmd_line_len)
{
if (cmd_line[i] == '\"')
{
i++;
while (cmd_line[i] != '\"' && i < cmd_line_len)
{
i++;
}
(*argc)++;
}
else
{
while (!isspace(cmd_line[i]) && i < cmd_line_len)
{
i++;
}
(*argc)++;
}
}
}

(*argv) = new char* [(*argc) + 1];
(*argv)[(*argc)] = NULL;

// Set the first arg to be the exec name

(*argv)[0] = new char [1024];
#ifdef _WIN32
::GetModuleFileName(0, (*argv)[0], 1024);
#else
(*argv)[0][0] = '\0';
#endif

// Allocate the others

int j;
for (j = 1; j < (*argc); j++)
*argc = 0;
*argv = nullptr;

// Split the command line to separate arguments
int numArgs = 0;
LPWSTR* wideArgs = nullptr;
// If cmd_line is an empty string then CommandLineToArgvW function returns the path to the current executable file,
// so we need to check if the string is empty.
if (lstrlenW(cmd_line) > 0)
{
wideArgs = CommandLineToArgvW(cmd_line, &numArgs);
if (wideArgs == nullptr)
{
(*argv)[j] = new char [cmd_line_len + 10];
return;
}
}

// Grab the args

size_t pos;
int argc_idx = 1;
// Allocate space for pointers in argv
(*argc) = numArgs + 1; // +1 because the first argument is the executable name
(*argv) = new char* [numArgs + 1];
for (int i = 0; i < numArgs + 1; i++)
{
(*argv)[i] = nullptr;
}

for (i = 0; i < cmd_line_len; i++)
if (wideArgs)
{
// Convert each argument to UTF8 and save it in argv
for (int i = 0; i < numArgs; ++i)
{
while (isspace(cmd_line[i]) && i < cmd_line_len)
BOOL lpUsedDefaultChar = false;
// Get length
int utf8length = WideCharToMultiByte(CP_UTF8, 0, wideArgs[i], -1, NULL, 0, NULL, &lpUsedDefaultChar);
char* utf8buffer = new char[utf8length + 1];
int retval = WideCharToMultiByte(CP_UTF8, 0, wideArgs[i], -1, utf8buffer, utf8length, NULL, &lpUsedDefaultChar);
if (!SUCCEEDED(retval))
{
i++;
}
if (i < cmd_line_len)
{
if (cmd_line[i] == '\"')
{
i++;
pos = i;
while (cmd_line[i] != '\"' && i < cmd_line_len)
{
i++;
}
memcpy((*argv)[argc_idx], &cmd_line[pos], i - pos);
(*argv)[argc_idx][i - pos] = '\0';
argc_idx++;
}
else
{
pos = i;
while (!isspace(cmd_line[i]) && i < cmd_line_len)
{
i++;
}
memcpy((*argv)[argc_idx], &cmd_line[pos], i - pos);
(*argv)[argc_idx][i - pos] = '\0';
argc_idx++;
}
// set to empty string in case of encoding error
utf8buffer[0] = '\0';
continue;
}
utf8buffer[utf8length] = '\0'; // Make sure the string is null-terminated
(*argv)[i + 1] = utf8buffer; // +1 because the first argument is the executable name
}
LocalFree(wideArgs);
}

// Get the application name
wchar_t wideBuffer[MAX_PATH];
DWORD wideLength = GetModuleFileNameW(NULL, wideBuffer, MAX_PATH);
// Convert the wide string to UTF-8
int utf8length = WideCharToMultiByte(CP_UTF8, 0, wideBuffer, wideLength, NULL, 0, NULL, NULL);
char* utf8buffer = new char[utf8length + 1];
WideCharToMultiByte(CP_UTF8, 0, wideBuffer, wideLength, utf8buffer, utf8length, NULL, NULL);
utf8buffer[utf8length] = '\0'; // Make sure the string is null-terminated
(*argv)[0] = utf8buffer;
}
#endif
10 changes: 9 additions & 1 deletion Base/ctkCommandLineParser.h
Original file line number Diff line number Diff line change
@@ -1,6 +1,10 @@
#ifndef __ctkCommandLineParser_h
#define __ctkCommandLineParser_h

#if defined (_WIN32)
#include <windows.h>
#endif

// Qt includes
#include <QString>
#include <QStringList>
Expand Down Expand Up @@ -383,14 +387,18 @@ class /*CTK_CORE_EXPORT*/ ctkCommandLineParser
*/
void setStrictModeEnabled(bool strictMode);

#if defined (_WIN32)
/**
* Convert windows-style arguments given as a command-line string
* into more traditional argc/argv arguments.
* If an argument is failed to be retrieved (for example, due to character encoding error)
* then the corresponding argv pointer is set to point to an empty string.
*
* @note argv[0] will be assigned the executable name using the ::GetModuleFileName function.
*/
static void convertWindowsCommandLineToUnixArguments(
const char *cmd_line, int *argc, char ***argv);
PWSTR cmd_line, int* argc, char*** argv);
#endif

private:
class ctkInternal;
Expand Down
4 changes: 2 additions & 2 deletions Base/ctkTest.h
Original file line number Diff line number Diff line change
Expand Up @@ -97,8 +97,8 @@ static void mouseEvent(QTest::MouseAction action, QWidget *widget, Qt::MouseButt
{
static const char *mouseActionNames[] =
{ "MousePress", "MouseRelease", "MouseClick", "MouseDClick", "MouseMove" };
QString warning = QString::fromLatin1("Mouse event \"%1\" not accepted by receiving widget");
QTest::qWarn(warning.arg(QString::fromLatin1(mouseActionNames[static_cast<int>(action)])).toLatin1());
QString warning = QLatin1String("Mouse event \"%1\" not accepted by receiving widget");
QTest::qWarn(warning.arg(QLatin1String(mouseActionNames[static_cast<int>(action)])).toLocal8Bit());
}
}

Expand Down
12 changes: 10 additions & 2 deletions Main.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -182,15 +182,23 @@ int appLauncherMain(int argc, char** argv)

// --------------------------------------------------------------------------
#ifndef CTKAPPLAUNCHER_WITHOUT_CONSOLE_IO_SUPPORT
// NOTE: On Windows, wmain should be used to allow non-ASCII characters in command-line arguments
int main(int argc, char *argv[])
{
// Uncomment the next two lines to stop at application start (to give a chance to connect with a debugger)
// std::cout << "Attach debugger and hit Enter" << std::endl;
// std::cin.get();

return appLauncherMain(argc, argv);
}
#elif defined Q_OS_WIN32
int __stdcall WinMain(HINSTANCE hInstance,
int __stdcall wWinMain(HINSTANCE hInstance,
HINSTANCE hPrevInstance,
LPSTR lpCmdLine, int nShowCmd)
PWSTR lpCmdLine, int nShowCmd)
{
// Uncomment the next line to stop at application start (to give a chance to connect with a debugger)
// int msgboxID = MessageBox(NULL, "Attach your debugger", "Debug", MB_ICONWARNING);

Q_UNUSED(hInstance);
Q_UNUSED(hPrevInstance);
Q_UNUSED(nShowCmd);
Expand Down
4 changes: 4 additions & 0 deletions msvc-static-configure.cmake
Original file line number Diff line number Diff line change
Expand Up @@ -29,6 +29,10 @@ elseif ("$ENV{APPLAUNCHER_CMAKE_GENERATOR}" STREQUAL "Visual Studio 16 2019")
# For local build with Visual Studio 2019 with modern CMake
set(APPLAUNCHER_CMAKE_GENERATOR -G "Visual Studio 16 2019" -T v141 -A Win32)
set(APPLAUNCHER_USE_NINJA OFF)
elseif ("$ENV{APPLAUNCHER_CMAKE_GENERATOR}" STREQUAL "Visual Studio 17 2022")
# For local build with Visual Studio 2019 with modern CMake
set(APPLAUNCHER_CMAKE_GENERATOR -G "Visual Studio 17 2022" -T v143 -A Win32)
set(APPLAUNCHER_USE_NINJA OFF)
else()
message(FATAL_ERROR "Env. variable APPLAUNCHER_CMAKE_GENERATOR is expected to match 'Ninja' or 'Visual Studio 15 2017' or 'Visual Studio 16 2019' [$ENV{APPLAUNCHER_CMAKE_GENERATOR}]")
endif()
Expand Down
Loading