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 g_utf16_len (const gunichar2 *str)
24 {
25 gsize result;
26
27 for (result = 0; str[0] != 0; str++, result++)
28 ;
29
30 return result;
31 }
32
33 static gunichar2 *
34 g_wcsdup (const gunichar2 *str, gssize str_len)
35 {
36 gsize str_len_unsigned;
37 gsize str_size;
38
39 g_return_val_if_fail (str != NULL, NULL);
40
41 if (str_len < 0)
42 str_len_unsigned = g_utf16_len (str);
43 else
44 str_len_unsigned = (gsize) str_len;
45
46 g_assert (str_len_unsigned <= G_MAXSIZE / sizeof (gunichar2) - 1);
47 str_size = (str_len_unsigned + 1) * sizeof (gunichar2);
48
49 return g_memdup2 (str, str_size);
50 }
51
52 static const gunichar2 *
53 g_utf16_wchr (const gunichar2 *str, const wchar_t wchr)
54 {
55 for (; str != NULL && str[0] != 0; str++)
56 if ((wchar_t) str[0] == wchr)
57 return str;
58
59 return NULL;
60 }
61
62 static gboolean
63 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 u8 = g_utf16_to_utf8 (str, length, NULL, NULL, NULL);
71
72 if (u8 == NULL)
73 return FALSE;
74
75 folded = g_utf8_casefold (u8, -1);
76
77 if (str_u8)
78 *str_u8 = g_steal_pointer (&u8);
79
80 g_free (u8);
81
82 if (str_u8_folded)
83 *str_u8_folded = g_steal_pointer (&folded);
84
85 g_free (folded);
86
87 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 g_utf16_find_basename (const gunichar2 *filename,
99 gssize len)
100 {
101 const gunichar2 *result;
102
103 if (len < 0)
104 len = g_utf16_len (filename);
105 if (len == 0)
106 return filename;
107
108 result = &filename[len - 1];
109
110 while (result > filename)
111 {
112 if ((wchar_t) result[0] == L'/' ||
113 (wchar_t) result[0] == L'\\')
114 {
115 result += 1;
116 break;
117 }
118
119 result -= 1;
120 }
121
122 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 g_utf8_find_basename (const gchar *filename,
134 gssize len)
135 {
136 const gchar *result;
137
138 if (len < 0)
139 len = strlen (filename);
140 if (len == 0)
141 return filename;
142
143 result = &filename[len - 1];
144
145 while (result > filename)
146 {
147 if (result[0] == '/' ||
148 result[0] == '\\')
149 {
150 result += 1;
151 break;
152 }
153
154 result -= 1;
155 }
156
157 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 _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 while ((wchar_t) commandline[0] == L' ')
185 commandline++;
186
187 quoted = FALSE;
188 execlen = 0;
189 found = FALSE;
190 first_argument = NULL;
191
192 if ((wchar_t) commandline[0] == L'"')
193 {
194 quoted = TRUE;
195 commandline += 1;
196 }
197
198 len = g_utf16_len (commandline);
199 p = commandline;
200
201 while (p < &commandline[len])
202 {
203 switch ((wchar_t) p[0])
204 {
205 case L'"':
206 if (quoted)
207 {
208 first_argument = p + 1;
209 /* Note: this is a valid commandline for opening "c:/file.txt":
210 * > "notepad"c:/file.txt
211 */
212 p = &commandline[len];
213 found = TRUE;
214 }
215 else
216 execlen += 1;
217 break;
218 case L' ':
219 if (!quoted)
220 {
221 first_argument = p;
222 p = &commandline[len];
223 found = TRUE;
224 }
225 else
226 execlen += 1;
227 break;
228 case L',':
229 if (!quoted && comma_separator)
230 {
231 first_argument = p;
232 p = &commandline[len];
233 found = TRUE;
234 }
235 else
236 execlen += 1;
237 break;
238 default:
239 execlen += 1;
240 break;
241 }
242 p += 1;
243 }
244
245 if (!found)
246 first_argument = &commandline[len];
247
248 if (executable_start)
249 *executable_start = commandline;
250
251 if (executable_len)
252 *executable_len = execlen;
253
254 if (executable_basename)
255 *executable_basename = g_utf16_find_basename (commandline, execlen);
256
257 if (after_executable)
258 *after_executable = first_argument;
259 }
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 _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 _g_win32_parse_filename (commandline, FALSE, &executable, &execlen, &executable_basename, &first_argument);
283
284 commandline = executable;
285
286 while ((wchar_t) first_argument[0] == L' ')
287 first_argument++;
288
289 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 g_assert (folded);
292
293 if (dll_function_out)
294 *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 if ((g_strcmp0 (ex_folded, "rundll32.exe") == 0 ||
304 g_str_has_suffix (ex_folded, "\\rundll32.exe") ||
305 g_str_has_suffix (ex_folded, "/rundll32.exe")) &&
306 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 const gunichar2 *filename_end = NULL;
335 gssize filename_len = 0;
336 gssize function_len = 0;
337 const gunichar2 *dllpart;
338
339 quoted = FALSE;
340
341 if ((wchar_t) first_argument[0] == L'"')
342 quoted = TRUE;
343
344 _g_win32_parse_filename (first_argument, TRUE, &dllpart, &filename_len, NULL, &filename_end);
345
346 if (filename_end[0] != 0 && filename_len > 0)
347 {
348 const gunichar2 *function_begin = filename_end;
349
350 while ((wchar_t) function_begin[0] == L',' || (wchar_t) function_begin[0] == L' ')
351 function_begin += 1;
352
353 if (function_begin[0] != 0)
354 {
355 gchar *dllpart_utf8;
356 gchar *dllpart_utf8_folded;
357 gchar *function_utf8;
358 const gunichar2 *space = g_utf16_wchr (function_begin, L' ');
359
360 if (space)
361 function_len = space - function_begin;
362 else
363 function_len = g_utf16_len (function_begin);
364
365 if (quoted)
366 first_argument += 1;
367
368 folded = g_utf16_to_utf8_and_fold (first_argument, filename_len, &dllpart_utf8, &dllpart_utf8_folded);
369 g_assert (folded);
370
371 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 *dll_function_out = g_steal_pointer (&function_utf8);
375
376 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 g_free (ex);
383 g_free (ex_folded);
384
385 ex = dllpart_utf8;
386 ex_folded = dllpart_utf8_folded;
387 }
388 }
389 }
390
391 if (ex_out)
392 {
393 if (ex_basename_out)
394 *ex_basename_out = (gchar *) g_utf8_find_basename (ex, -1);
395
396 *ex_out = g_steal_pointer (&ex);
397 }
398
399 g_free (ex);
400
401 if (ex_folded_out)
402 {
403 if (ex_folded_basename_out)
404 *ex_folded_basename_out = (gchar *) g_utf8_find_basename (ex_folded, -1);
405
406 *ex_folded_out = g_steal_pointer (&ex_folded);
407 }
408
409 g_free (ex_folded);
410 }
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 _g_win32_fixup_broken_microsoft_rundll_commandline (gunichar2 *commandline)
435 {
436 const gunichar2 *first_argument;
437 gunichar2 *after_first_argument;
438
439 _g_win32_parse_filename (commandline, FALSE, NULL, NULL, NULL, &first_argument);
440
441 while ((wchar_t) first_argument[0] == L' ')
442 first_argument++;
443
444 _g_win32_parse_filename (first_argument, TRUE, NULL, NULL, NULL, (const gunichar2 **) &after_first_argument);
445
446 if ((wchar_t) after_first_argument[0] == L',')
447 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 }