Branch data Line data Source code
1 : : /* giowin32-private.c - private glib-gio functions for W32 GAppInfo
2 : : *
3 : : * Copyright 2019 Руслан Ижбулатов
4 : : *
5 : : * SPDX-License-Identifier: LGPL-2.1-or-later
6 : : *
7 : : * This library is free software; you can redistribute it and/or
8 : : * modify it under the terms of the GNU Lesser General Public
9 : : * License as published by the Free Software Foundation; either
10 : : * version 2.1 of the License, or (at your option) any later version.
11 : : *
12 : : * This library is distributed in the hope that it will be useful,
13 : : * but WITHOUT ANY WARRANTY; without even the implied warranty of
14 : : * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
15 : : * Lesser General Public License for more details.
16 : : *
17 : : * You should have received a copy of the GNU Lesser General Public License
18 : : * along with this library; if not, see <http://www.gnu.org/licenses/>.
19 : : */
20 : :
21 : :
22 : : static gsize
23 : 171 : g_utf16_len (const gunichar2 *str)
24 : : {
25 : : gsize result;
26 : :
27 : 6914 : for (result = 0; str[0] != 0; str++, result++)
28 : : ;
29 : :
30 : 171 : return result;
31 : : }
32 : :
33 : : static gunichar2 *
34 : 6 : g_wcsdup (const gunichar2 *str, gssize str_len)
35 : : {
36 : : gsize str_len_unsigned;
37 : : gsize str_size;
38 : :
39 : 6 : g_return_val_if_fail (str != NULL, NULL);
40 : :
41 : 6 : if (str_len < 0)
42 : 3 : str_len_unsigned = g_utf16_len (str);
43 : : else
44 : 3 : str_len_unsigned = (gsize) str_len;
45 : :
46 : 6 : g_assert (str_len_unsigned <= G_MAXSIZE / sizeof (gunichar2) - 1);
47 : 6 : str_size = (str_len_unsigned + 1) * sizeof (gunichar2);
48 : :
49 : 6 : return g_memdup2 (str, str_size);
50 : : }
51 : :
52 : : static const gunichar2 *
53 : 17 : g_utf16_wchr (const gunichar2 *str, const wchar_t wchr)
54 : : {
55 : 174 : for (; str != NULL && str[0] != 0; str++)
56 : 163 : if ((wchar_t) str[0] == wchr)
57 : 6 : return str;
58 : :
59 : 11 : return NULL;
60 : : }
61 : :
62 : : static gboolean
63 : 98 : g_utf16_to_utf8_and_fold (const gunichar2 *str,
64 : : gssize length,
65 : : gchar **str_u8,
66 : : gchar **str_u8_folded)
67 : : {
68 : : gchar *u8;
69 : : gchar *folded;
70 : 98 : u8 = g_utf16_to_utf8 (str, length, NULL, NULL, NULL);
71 : :
72 : 98 : if (u8 == NULL)
73 : 3 : return FALSE;
74 : :
75 : 95 : folded = g_utf8_casefold (u8, -1);
76 : :
77 : 95 : if (str_u8)
78 : 93 : *str_u8 = g_steal_pointer (&u8);
79 : :
80 : 95 : g_free (u8);
81 : :
82 : 95 : if (str_u8_folded)
83 : 91 : *str_u8_folded = g_steal_pointer (&folded);
84 : :
85 : 95 : g_free (folded);
86 : :
87 : 95 : return TRUE;
88 : : }
89 : :
90 : : /* Finds the last directory separator in @filename,
91 : : * returns a pointer to the position after that separator.
92 : : * If the string ends with a separator, returned value
93 : : * will be pointing at the NUL terminator.
94 : : * If the string does not contain separators, returns the
95 : : * string itself.
96 : : */
97 : : static const gunichar2 *
98 : 75 : g_utf16_find_basename (const gunichar2 *filename,
99 : : gssize len)
100 : : {
101 : : const gunichar2 *result;
102 : :
103 : 75 : if (len < 0)
104 : 3 : len = g_utf16_len (filename);
105 : 75 : if (len == 0)
106 : 1 : return filename;
107 : :
108 : 74 : result = &filename[len - 1];
109 : :
110 : 891 : while (result > filename)
111 : : {
112 : 835 : if ((wchar_t) result[0] == L'/' ||
113 : 829 : (wchar_t) result[0] == L'\\')
114 : : {
115 : 18 : result += 1;
116 : 18 : break;
117 : : }
118 : :
119 : 817 : result -= 1;
120 : : }
121 : :
122 : 74 : return result;
123 : : }
124 : :
125 : : /* Finds the last directory separator in @filename,
126 : : * returns a pointer to the position after that separator.
127 : : * If the string ends with a separator, returned value
128 : : * will be pointing at the NUL terminator.
129 : : * If the string does not contain separators, returns the
130 : : * string itself.
131 : : */
132 : : static const gchar *
133 : 50 : g_utf8_find_basename (const gchar *filename,
134 : : gssize len)
135 : : {
136 : : const gchar *result;
137 : :
138 : 50 : if (len < 0)
139 : 48 : len = strlen (filename);
140 : 50 : if (len == 0)
141 : 1 : return filename;
142 : :
143 : 49 : result = &filename[len - 1];
144 : :
145 : 593 : while (result > filename)
146 : : {
147 : 556 : if (result[0] == '/' ||
148 : 552 : result[0] == '\\')
149 : : {
150 : 12 : result += 1;
151 : 12 : break;
152 : : }
153 : :
154 : 544 : result -= 1;
155 : : }
156 : :
157 : 49 : return result;
158 : : }
159 : :
160 : : /**
161 : : * Parses @commandline, figuring out what the filename being invoked
162 : : * is. All returned strings are pointers into @commandline.
163 : : * @commandline must be a valid UTF-16 string and not be NULL.
164 : : * @after_executable is the first character after executable
165 : : * (usually a space, but not always).
166 : : * If @comma_separator is TRUE, accepts ',' as a separator between
167 : : * the filename and the following argument.
168 : : */
169 : : static void
170 : 151 : _g_win32_parse_filename (const gunichar2 *commandline,
171 : : gboolean comma_separator,
172 : : const gunichar2 **executable_start,
173 : : gssize *executable_len,
174 : : const gunichar2 **executable_basename,
175 : : const gunichar2 **after_executable)
176 : : {
177 : : const gunichar2 *p;
178 : : const gunichar2 *first_argument;
179 : : gboolean quoted;
180 : : gssize len;
181 : : gssize execlen;
182 : : gboolean found;
183 : :
184 : 181 : while ((wchar_t) commandline[0] == L' ')
185 : 30 : commandline++;
186 : :
187 : 151 : quoted = FALSE;
188 : 151 : execlen = 0;
189 : 151 : found = FALSE;
190 : 151 : first_argument = NULL;
191 : :
192 : 151 : if ((wchar_t) commandline[0] == L'"')
193 : : {
194 : 74 : quoted = TRUE;
195 : 74 : commandline += 1;
196 : : }
197 : :
198 : 151 : len = g_utf16_len (commandline);
199 : 151 : p = commandline;
200 : :
201 : 3090 : while (p < &commandline[len])
202 : : {
203 : 2939 : switch ((wchar_t) p[0])
204 : : {
205 : 78 : case L'"':
206 : 78 : if (quoted)
207 : : {
208 : 74 : first_argument = p + 1;
209 : : /* Note: this is a valid commandline for opening "c:/file.txt":
210 : : * > "notepad"c:/file.txt
211 : : */
212 : 74 : p = &commandline[len];
213 : 74 : found = TRUE;
214 : : }
215 : : else
216 : 4 : execlen += 1;
217 : 78 : break;
218 : 121 : case L' ':
219 : 121 : if (!quoted)
220 : : {
221 : 59 : first_argument = p;
222 : 59 : p = &commandline[len];
223 : 59 : found = TRUE;
224 : : }
225 : : else
226 : 62 : execlen += 1;
227 : 121 : break;
228 : 21 : case L',':
229 : 21 : if (!quoted && comma_separator)
230 : : {
231 : 13 : first_argument = p;
232 : 13 : p = &commandline[len];
233 : 13 : found = TRUE;
234 : : }
235 : : else
236 : 8 : execlen += 1;
237 : 21 : break;
238 : 2719 : default:
239 : 2719 : execlen += 1;
240 : 2719 : break;
241 : : }
242 : 2939 : p += 1;
243 : : }
244 : :
245 : 151 : if (!found)
246 : 5 : first_argument = &commandline[len];
247 : :
248 : 151 : if (executable_start)
249 : 93 : *executable_start = commandline;
250 : :
251 : 151 : if (executable_len)
252 : 93 : *executable_len = execlen;
253 : :
254 : 151 : if (executable_basename)
255 : 72 : *executable_basename = g_utf16_find_basename (commandline, execlen);
256 : :
257 : 151 : if (after_executable)
258 : 127 : *after_executable = first_argument;
259 : 151 : }
260 : :
261 : : /* Make sure @commandline is a valid UTF-16 string before
262 : : * calling this function!
263 : : * follow_class_chain_to_handler() does perform such validation.
264 : : */
265 : : static void
266 : 72 : _g_win32_extract_executable (const gunichar2 *commandline,
267 : : gchar **ex_out,
268 : : gchar **ex_basename_out,
269 : : gchar **ex_folded_out,
270 : : gchar **ex_folded_basename_out,
271 : : gchar **dll_function_out)
272 : : {
273 : : gchar *ex;
274 : : gchar *ex_folded;
275 : : const gunichar2 *first_argument;
276 : : const gunichar2 *executable;
277 : : const gunichar2 *executable_basename;
278 : : gboolean quoted;
279 : : gboolean folded;
280 : : gssize execlen;
281 : :
282 : 72 : _g_win32_parse_filename (commandline, FALSE, &executable, &execlen, &executable_basename, &first_argument);
283 : :
284 : 72 : commandline = executable;
285 : :
286 : 144 : while ((wchar_t) first_argument[0] == L' ')
287 : 72 : first_argument++;
288 : :
289 : 72 : folded = g_utf16_to_utf8_and_fold (executable, (gssize) execlen, &ex, &ex_folded);
290 : : /* This should never fail as @executable has to be valid UTF-16. */
291 : 72 : g_assert (folded);
292 : :
293 : 72 : if (dll_function_out)
294 : 24 : *dll_function_out = NULL;
295 : :
296 : : /* See if the executable basename is "rundll32.exe". If so, then
297 : : * parse the rest of the commandline as r'"?path-to-dll"?[ ]*,*[ ]*dll_function_to_invoke'
298 : : */
299 : : /* Using just "rundll32.exe", without an absolute path, seems
300 : : * very exploitable, but MS does that sometimes, so we have
301 : : * to accept that.
302 : : */
303 : 72 : if ((g_strcmp0 (ex_folded, "rundll32.exe") == 0 ||
304 : 24 : g_str_has_suffix (ex_folded, "\\rundll32.exe") ||
305 : 12 : g_str_has_suffix (ex_folded, "/rundll32.exe")) &&
306 : 66 : first_argument[0] != 0 &&
307 : : dll_function_out != NULL)
308 : : {
309 : : /* Corner cases:
310 : : * > rundll32.exe c:\some,file,with,commas.dll,some_function
311 : : * is treated by rundll32 as:
312 : : * dll=c:\some
313 : : * function=file,with,commas.dll,some_function
314 : : * unless the dll name is surrounded by double quotation marks:
315 : : * > rundll32.exe "c:\some,file,with,commas.dll",some_function
316 : : * in which case everything works normally.
317 : : * Also, quoting only works if it surrounds the file name, i.e:
318 : : * > rundll32.exe "c:\some,file"",with,commas.dll",some_function
319 : : * will not work.
320 : : * Also, comma is optional when filename is quoted or when function
321 : : * name is separated from the filename by space(s):
322 : : * > rundll32.exe "c:\some,file,with,commas.dll"some_function
323 : : * will work,
324 : : * > rundll32.exe c:\some_dll_without_commas_or_spaces.dll some_function
325 : : * will work too.
326 : : * Also, any number of commas is accepted:
327 : : * > rundll32.exe c:\some_dll_without_commas_or_spaces.dll , , ,,, , some_function
328 : : * works just fine.
329 : : * And the ultimate example is:
330 : : * > "rundll32.exe""c:\some,file,with,commas.dll"some_function
331 : : * and it also works.
332 : : * Good job, Microsoft!
333 : : */
334 : 21 : const gunichar2 *filename_end = NULL;
335 : 21 : gssize filename_len = 0;
336 : 21 : gssize function_len = 0;
337 : : const gunichar2 *dllpart;
338 : :
339 : 21 : quoted = FALSE;
340 : :
341 : 21 : if ((wchar_t) first_argument[0] == L'"')
342 : 10 : quoted = TRUE;
343 : :
344 : 21 : _g_win32_parse_filename (first_argument, TRUE, &dllpart, &filename_len, NULL, &filename_end);
345 : :
346 : 21 : if (filename_end[0] != 0 && filename_len > 0)
347 : : {
348 : 18 : const gunichar2 *function_begin = filename_end;
349 : :
350 : 60 : while ((wchar_t) function_begin[0] == L',' || (wchar_t) function_begin[0] == L' ')
351 : 42 : function_begin += 1;
352 : :
353 : 18 : if (function_begin[0] != 0)
354 : : {
355 : : gchar *dllpart_utf8;
356 : : gchar *dllpart_utf8_folded;
357 : : gchar *function_utf8;
358 : 17 : const gunichar2 *space = g_utf16_wchr (function_begin, L' ');
359 : :
360 : 17 : if (space)
361 : 6 : function_len = space - function_begin;
362 : : else
363 : 11 : function_len = g_utf16_len (function_begin);
364 : :
365 : 17 : if (quoted)
366 : 10 : first_argument += 1;
367 : :
368 : 17 : folded = g_utf16_to_utf8_and_fold (first_argument, filename_len, &dllpart_utf8, &dllpart_utf8_folded);
369 : 17 : g_assert (folded);
370 : :
371 : 17 : function_utf8 = g_utf16_to_utf8 (function_begin, function_len, NULL, NULL, NULL);
372 : :
373 : : /* We only take this branch when dll_function_out is not NULL */
374 : 17 : *dll_function_out = g_steal_pointer (&function_utf8);
375 : :
376 : 17 : g_free (function_utf8);
377 : :
378 : : /*
379 : : * Free our previous output candidate (rundll32) and replace it with the DLL path,
380 : : * then proceed forward as if nothing has changed.
381 : : */
382 : 17 : g_free (ex);
383 : 17 : g_free (ex_folded);
384 : :
385 : 17 : ex = dllpart_utf8;
386 : 17 : ex_folded = dllpart_utf8_folded;
387 : : }
388 : : }
389 : : }
390 : :
391 : 72 : if (ex_out)
392 : : {
393 : 48 : if (ex_basename_out)
394 : 24 : *ex_basename_out = (gchar *) g_utf8_find_basename (ex, -1);
395 : :
396 : 48 : *ex_out = g_steal_pointer (&ex);
397 : : }
398 : :
399 : 72 : g_free (ex);
400 : :
401 : 72 : if (ex_folded_out)
402 : : {
403 : 48 : if (ex_folded_basename_out)
404 : 24 : *ex_folded_basename_out = (gchar *) g_utf8_find_basename (ex_folded, -1);
405 : :
406 : 48 : *ex_folded_out = g_steal_pointer (&ex_folded);
407 : : }
408 : :
409 : 72 : g_free (ex_folded);
410 : 72 : }
411 : :
412 : : /**
413 : : * rundll32 accepts many different commandlines. Among them is this:
414 : : * > rundll32.exe "c:/program files/foo/bar.dll",,, , ,,,, , function_name %1
415 : : * rundll32 just reads the first argument as a potentially quoted
416 : : * filename until the quotation ends (if quoted) or until a comma,
417 : : * or until a space. Then ignores all subsequent spaces (if any) and commas (if any;
418 : : * at least one comma is mandatory only if the filename is not quoted),
419 : : * and then interprets the rest of the commandline (until a space or a NUL-byte)
420 : : * as a name of a function.
421 : : * When GLib tries to run a program, it attempts to correctly re-quote the arguments,
422 : : * turning the first argument into "c:/program files/foo/bar.dll,,,".
423 : : * This breaks rundll32 parsing logic.
424 : : * Try to work around this by ensuring that the syntax is like this:
425 : : * > rundll32.exe "c:/program files/foo/bar.dll" function_name
426 : : * This syntax is valid for rundll32 *and* GLib spawn routines won't break it.
427 : : *
428 : : * @commandline must have at least 2 arguments, and the second argument
429 : : * must contain a (possibly quoted) filename, followed by a space or
430 : : * a comma. This can be checked for with an extract_executable() call -
431 : : * it should return a non-null dll_function.
432 : : */
433 : : static void
434 : 17 : _g_win32_fixup_broken_microsoft_rundll_commandline (gunichar2 *commandline)
435 : : {
436 : : const gunichar2 *first_argument;
437 : : gunichar2 *after_first_argument;
438 : :
439 : 17 : _g_win32_parse_filename (commandline, FALSE, NULL, NULL, NULL, &first_argument);
440 : :
441 : 33 : while ((wchar_t) first_argument[0] == L' ')
442 : 16 : first_argument++;
443 : :
444 : 17 : _g_win32_parse_filename (first_argument, TRUE, NULL, NULL, NULL, (const gunichar2 **) &after_first_argument);
445 : :
446 : 17 : if ((wchar_t) after_first_argument[0] == L',')
447 : 13 : after_first_argument[0] = 0x0020;
448 : : /* Else everything is ok (first char after filename is ' ' or the first char
449 : : * of the function name - either way this will work).
450 : : */
451 : 17 : }
|