/* giowin32-private.c - private glib-gio functions for W32 GAppInfo * * Copyright 2019 Руслан Ижбулатов * * SPDX-License-Identifier: LGPL-2.1-or-later * * This library is free software; you can redistribute it and/or * modify it under the terms of the GNU Lesser General Public * License as published by the Free Software Foundation; either * version 2.1 of the License, or (at your option) any later version. * * This library is distributed in the hope that it will be useful, * but WITHOUT ANY WARRANTY; without even the implied warranty of * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU * Lesser General Public License for more details. * * You should have received a copy of the GNU Lesser General Public License * along with this library; if not, see . */ static gsize g_utf16_len (const gunichar2 *str) { gsize result; for (result = 0; str[0] != 0; str++, result++) ; return result; } static gunichar2 * g_wcsdup (const gunichar2 *str, gssize str_len) { gsize str_len_unsigned; gsize str_size; g_return_val_if_fail (str != NULL, NULL); if (str_len < 0) str_len_unsigned = g_utf16_len (str); else str_len_unsigned = (gsize) str_len; g_assert (str_len_unsigned <= G_MAXSIZE / sizeof (gunichar2) - 1); str_size = (str_len_unsigned + 1) * sizeof (gunichar2); return g_memdup2 (str, str_size); } static const gunichar2 * g_utf16_wchr (const gunichar2 *str, const wchar_t wchr) { for (; str != NULL && str[0] != 0; str++) if ((wchar_t) str[0] == wchr) return str; return NULL; } static gboolean g_utf16_to_utf8_and_fold (const gunichar2 *str, gssize length, gchar **str_u8, gchar **str_u8_folded) { gchar *u8; gchar *folded; u8 = g_utf16_to_utf8 (str, length, NULL, NULL, NULL); if (u8 == NULL) return FALSE; folded = g_utf8_casefold (u8, -1); if (str_u8) *str_u8 = g_steal_pointer (&u8); g_free (u8); if (str_u8_folded) *str_u8_folded = g_steal_pointer (&folded); g_free (folded); return TRUE; } /* Finds the last directory separator in @filename, * returns a pointer to the position after that separator. * If the string ends with a separator, returned value * will be pointing at the NUL terminator. * If the string does not contain separators, returns the * string itself. */ static const gunichar2 * g_utf16_find_basename (const gunichar2 *filename, gssize len) { const gunichar2 *result; if (len < 0) len = g_utf16_len (filename); if (len == 0) return filename; result = &filename[len - 1]; while (result > filename) { if ((wchar_t) result[0] == L'/' || (wchar_t) result[0] == L'\\') { result += 1; break; } result -= 1; } return result; } /* Finds the last directory separator in @filename, * returns a pointer to the position after that separator. * If the string ends with a separator, returned value * will be pointing at the NUL terminator. * If the string does not contain separators, returns the * string itself. */ static const gchar * g_utf8_find_basename (const gchar *filename, gssize len) { const gchar *result; if (len < 0) len = strlen (filename); if (len == 0) return filename; result = &filename[len - 1]; while (result > filename) { if (result[0] == '/' || result[0] == '\\') { result += 1; break; } result -= 1; } return result; } /** * Parses @commandline, figuring out what the filename being invoked * is. All returned strings are pointers into @commandline. * @commandline must be a valid UTF-16 string and not be NULL. * @after_executable is the first character after executable * (usually a space, but not always). * If @comma_separator is TRUE, accepts ',' as a separator between * the filename and the following argument. */ static void _g_win32_parse_filename (const gunichar2 *commandline, gboolean comma_separator, const gunichar2 **executable_start, gssize *executable_len, const gunichar2 **executable_basename, const gunichar2 **after_executable) { const gunichar2 *p; const gunichar2 *first_argument; gboolean quoted; gssize len; gssize execlen; gboolean found; while ((wchar_t) commandline[0] == L' ') commandline++; quoted = FALSE; execlen = 0; found = FALSE; first_argument = NULL; if ((wchar_t) commandline[0] == L'"') { quoted = TRUE; commandline += 1; } len = g_utf16_len (commandline); p = commandline; while (p < &commandline[len]) { switch ((wchar_t) p[0]) { case L'"': if (quoted) { first_argument = p + 1; /* Note: this is a valid commandline for opening "c:/file.txt": * > "notepad"c:/file.txt */ p = &commandline[len]; found = TRUE; } else execlen += 1; break; case L' ': if (!quoted) { first_argument = p; p = &commandline[len]; found = TRUE; } else execlen += 1; break; case L',': if (!quoted && comma_separator) { first_argument = p; p = &commandline[len]; found = TRUE; } else execlen += 1; break; default: execlen += 1; break; } p += 1; } if (!found) first_argument = &commandline[len]; if (executable_start) *executable_start = commandline; if (executable_len) *executable_len = execlen; if (executable_basename) *executable_basename = g_utf16_find_basename (commandline, execlen); if (after_executable) *after_executable = first_argument; } /* Make sure @commandline is a valid UTF-16 string before * calling this function! * follow_class_chain_to_handler() does perform such validation. */ static void _g_win32_extract_executable (const gunichar2 *commandline, gchar **ex_out, gchar **ex_basename_out, gchar **ex_folded_out, gchar **ex_folded_basename_out, gchar **dll_function_out) { gchar *ex; gchar *ex_folded; const gunichar2 *first_argument; const gunichar2 *executable; const gunichar2 *executable_basename; gboolean quoted; gboolean folded; gssize execlen; _g_win32_parse_filename (commandline, FALSE, &executable, &execlen, &executable_basename, &first_argument); commandline = executable; while ((wchar_t) first_argument[0] == L' ') first_argument++; folded = g_utf16_to_utf8_and_fold (executable, (gssize) execlen, &ex, &ex_folded); /* This should never fail as @executable has to be valid UTF-16. */ g_assert (folded); if (dll_function_out) *dll_function_out = NULL; /* See if the executable basename is "rundll32.exe". If so, then * parse the rest of the commandline as r'"?path-to-dll"?[ ]*,*[ ]*dll_function_to_invoke' */ /* Using just "rundll32.exe", without an absolute path, seems * very exploitable, but MS does that sometimes, so we have * to accept that. */ if ((g_strcmp0 (ex_folded, "rundll32.exe") == 0 || g_str_has_suffix (ex_folded, "\\rundll32.exe") || g_str_has_suffix (ex_folded, "/rundll32.exe")) && first_argument[0] != 0 && dll_function_out != NULL) { /* Corner cases: * > rundll32.exe c:\some,file,with,commas.dll,some_function * is treated by rundll32 as: * dll=c:\some * function=file,with,commas.dll,some_function * unless the dll name is surrounded by double quotation marks: * > rundll32.exe "c:\some,file,with,commas.dll",some_function * in which case everything works normally. * Also, quoting only works if it surrounds the file name, i.e: * > rundll32.exe "c:\some,file"",with,commas.dll",some_function * will not work. * Also, comma is optional when filename is quoted or when function * name is separated from the filename by space(s): * > rundll32.exe "c:\some,file,with,commas.dll"some_function * will work, * > rundll32.exe c:\some_dll_without_commas_or_spaces.dll some_function * will work too. * Also, any number of commas is accepted: * > rundll32.exe c:\some_dll_without_commas_or_spaces.dll , , ,,, , some_function * works just fine. * And the ultimate example is: * > "rundll32.exe""c:\some,file,with,commas.dll"some_function * and it also works. * Good job, Microsoft! */ const gunichar2 *filename_end = NULL; gssize filename_len = 0; gssize function_len = 0; const gunichar2 *dllpart; quoted = FALSE; if ((wchar_t) first_argument[0] == L'"') quoted = TRUE; _g_win32_parse_filename (first_argument, TRUE, &dllpart, &filename_len, NULL, &filename_end); if (filename_end[0] != 0 && filename_len > 0) { const gunichar2 *function_begin = filename_end; while ((wchar_t) function_begin[0] == L',' || (wchar_t) function_begin[0] == L' ') function_begin += 1; if (function_begin[0] != 0) { gchar *dllpart_utf8; gchar *dllpart_utf8_folded; gchar *function_utf8; const gunichar2 *space = g_utf16_wchr (function_begin, L' '); if (space) function_len = space - function_begin; else function_len = g_utf16_len (function_begin); if (quoted) first_argument += 1; folded = g_utf16_to_utf8_and_fold (first_argument, filename_len, &dllpart_utf8, &dllpart_utf8_folded); g_assert (folded); function_utf8 = g_utf16_to_utf8 (function_begin, function_len, NULL, NULL, NULL); /* We only take this branch when dll_function_out is not NULL */ *dll_function_out = g_steal_pointer (&function_utf8); g_free (function_utf8); /* * Free our previous output candidate (rundll32) and replace it with the DLL path, * then proceed forward as if nothing has changed. */ g_free (ex); g_free (ex_folded); ex = dllpart_utf8; ex_folded = dllpart_utf8_folded; } } } if (ex_out) { if (ex_basename_out) *ex_basename_out = (gchar *) g_utf8_find_basename (ex, -1); *ex_out = g_steal_pointer (&ex); } g_free (ex); if (ex_folded_out) { if (ex_folded_basename_out) *ex_folded_basename_out = (gchar *) g_utf8_find_basename (ex_folded, -1); *ex_folded_out = g_steal_pointer (&ex_folded); } g_free (ex_folded); } /** * rundll32 accepts many different commandlines. Among them is this: * > rundll32.exe "c:/program files/foo/bar.dll",,, , ,,,, , function_name %1 * rundll32 just reads the first argument as a potentially quoted * filename until the quotation ends (if quoted) or until a comma, * or until a space. Then ignores all subsequent spaces (if any) and commas (if any; * at least one comma is mandatory only if the filename is not quoted), * and then interprets the rest of the commandline (until a space or a NUL-byte) * as a name of a function. * When GLib tries to run a program, it attempts to correctly re-quote the arguments, * turning the first argument into "c:/program files/foo/bar.dll,,,". * This breaks rundll32 parsing logic. * Try to work around this by ensuring that the syntax is like this: * > rundll32.exe "c:/program files/foo/bar.dll" function_name * This syntax is valid for rundll32 *and* GLib spawn routines won't break it. * * @commandline must have at least 2 arguments, and the second argument * must contain a (possibly quoted) filename, followed by a space or * a comma. This can be checked for with an extract_executable() call - * it should return a non-null dll_function. */ static void _g_win32_fixup_broken_microsoft_rundll_commandline (gunichar2 *commandline) { const gunichar2 *first_argument; gunichar2 *after_first_argument; _g_win32_parse_filename (commandline, FALSE, NULL, NULL, NULL, &first_argument); while ((wchar_t) first_argument[0] == L' ') first_argument++; _g_win32_parse_filename (first_argument, TRUE, NULL, NULL, NULL, (const gunichar2 **) &after_first_argument); if ((wchar_t) after_first_argument[0] == L',') after_first_argument[0] = 0x0020; /* Else everything is ok (first char after filename is ' ' or the first char * of the function name - either way this will work). */ }