47e840805278c75ad56d2532e7115fcaabd4f168
[platform/upstream/glib.git] / gio / giowin32-private.c
1 /* giowin32-private.c - private glib-gio functions for W32 GAppInfo
2  *
3  * Copyright 2019 Руслан Ижбулатов
4  *
5  * This library is free software; you can redistribute it and/or
6  * modify it under the terms of the GNU Lesser General Public
7  * License as published by the Free Software Foundation; either
8  * version 2.1 of the License, or (at your option) any later version.
9  *
10  * This library is distributed in the hope that it will be useful,
11  * but WITHOUT ANY WARRANTY; without even the implied warranty of
12  * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the GNU
13  * Lesser General Public License for more details.
14  *
15  * You should have received a copy of the GNU Lesser General Public License
16  * along with this library; if not, see <http://www.gnu.org/licenses/>.
17  */
18
19 #include "gstrfuncsprivate.h"
20
21 static gsize
22 g_utf16_len (const gunichar2 *str)
23 {
24   gsize result;
25
26   for (result = 0; str[0] != 0; str++, result++)
27     ;
28
29   return result;
30 }
31
32 static gunichar2 *
33 g_wcsdup (const gunichar2 *str, gssize str_len)
34 {
35   gsize str_len_unsigned;
36   gsize str_size;
37
38   g_return_val_if_fail (str != NULL, NULL);
39
40   if (str_len < 0)
41     str_len_unsigned = g_utf16_len (str);
42   else
43     str_len_unsigned = (gsize) str_len;
44
45   g_assert (str_len_unsigned <= G_MAXSIZE / sizeof (gunichar2) - 1);
46   str_size = (str_len_unsigned + 1) * sizeof (gunichar2);
47
48   return g_memdup2 (str, str_size);
49 }
50
51 static const gunichar2 *
52 g_utf16_wchr (const gunichar2 *str, const wchar_t wchr)
53 {
54   for (; str != NULL && str[0] != 0; str++)
55     if ((wchar_t) str[0] == wchr)
56       return str;
57
58   return NULL;
59 }
60
61 static gboolean
62 g_utf16_to_utf8_and_fold (const gunichar2  *str,
63                           gssize            length,
64                           gchar           **str_u8,
65                           gchar           **str_u8_folded)
66 {
67   gchar *u8;
68   gchar *folded;
69   u8 = g_utf16_to_utf8 (str, length, NULL, NULL, NULL);
70
71   if (u8 == NULL)
72     return FALSE;
73
74   folded = g_utf8_casefold (u8, -1);
75
76   if (str_u8)
77     *str_u8 = g_steal_pointer (&u8);
78
79   g_free (u8);
80
81   if (str_u8_folded)
82     *str_u8_folded = g_steal_pointer (&folded);
83
84   g_free (folded);
85
86   return TRUE;
87 }
88
89 /* Finds the last directory separator in @filename,
90  * returns a pointer to the position after that separator.
91  * If the string ends with a separator, returned value
92  * will be pointing at the NUL terminator.
93  * If the string does not contain separators, returns the
94  * string itself.
95  */
96 static const gunichar2 *
97 g_utf16_find_basename (const gunichar2 *filename,
98                        gssize           len)
99 {
100   const gunichar2 *result;
101
102   if (len < 0)
103     len = g_utf16_len (filename);
104   if (len == 0)
105     return filename;
106
107   result = &filename[len - 1];
108
109   while (result > filename)
110     {
111       if ((wchar_t) result[0] == L'/' ||
112           (wchar_t) result[0] == L'\\')
113         {
114           result += 1;
115           break;
116         }
117
118       result -= 1;
119     }
120
121   return result;
122 }
123
124 /* Finds the last directory separator in @filename,
125  * returns a pointer to the position after that separator.
126  * If the string ends with a separator, returned value
127  * will be pointing at the NUL terminator.
128  * If the string does not contain separators, returns the
129  * string itself.
130  */
131 static const gchar *
132 g_utf8_find_basename (const gchar *filename,
133                       gssize       len)
134 {
135   const gchar *result;
136
137   if (len < 0)
138     len = strlen (filename);
139   if (len == 0)
140     return filename;
141
142   result = &filename[len - 1];
143
144   while (result > filename)
145     {
146       if (result[0] == '/' ||
147           result[0] == '\\')
148         {
149           result += 1;
150           break;
151         }
152
153       result -= 1;
154     }
155
156   return result;
157 }
158
159 /**
160  * Parses @commandline, figuring out what the filename being invoked
161  * is. All returned strings are pointers into @commandline.
162  * @commandline must be a valid UTF-16 string and not be NULL.
163  * @after_executable is the first character after executable
164  * (usually a space, but not always).
165  * If @comma_separator is TRUE, accepts ',' as a separator between
166  * the filename and the following argument.
167  */
168 static void
169 _g_win32_parse_filename (const gunichar2  *commandline,
170                          gboolean          comma_separator,
171                          const gunichar2 **executable_start,
172                          gssize           *executable_len,
173                          const gunichar2 **executable_basename,
174                          const gunichar2 **after_executable)
175 {
176   const gunichar2 *p;
177   const gunichar2 *first_argument;
178   gboolean quoted;
179   gssize len;
180   gssize execlen;
181   gboolean found;
182
183   while ((wchar_t) commandline[0] == L' ')
184     commandline++;
185
186   quoted = FALSE;
187   execlen = 0;
188   found = FALSE;
189   first_argument = NULL;
190
191   if ((wchar_t) commandline[0] == L'"')
192     {
193       quoted = TRUE;
194       commandline += 1;
195     }
196
197   len = g_utf16_len (commandline);
198   p = commandline;
199
200   while (p < &commandline[len])
201     {
202       switch ((wchar_t) p[0])
203         {
204         case L'"':
205           if (quoted)
206             {
207               first_argument = p + 1;
208               /* Note: this is a valid commandline for opening "c:/file.txt":
209                * > "notepad"c:/file.txt
210                */
211               p = &commandline[len];
212               found = TRUE;
213             }
214           else
215             execlen += 1;
216           break;
217         case L' ':
218           if (!quoted)
219             {
220               first_argument = p;
221               p = &commandline[len];
222               found = TRUE;
223             }
224           else
225             execlen += 1;
226           break;
227         case L',':
228           if (!quoted && comma_separator)
229             {
230               first_argument = p;
231               p = &commandline[len];
232               found = TRUE;
233             }
234           else
235             execlen += 1;
236           break;
237         default:
238           execlen += 1;
239           break;
240         }
241       p += 1;
242     }
243
244   if (!found)
245     first_argument = &commandline[len];
246
247   if (executable_start)
248     *executable_start = commandline;
249
250   if (executable_len)
251     *executable_len = execlen;
252
253   if (executable_basename)
254     *executable_basename = g_utf16_find_basename (commandline, execlen);
255
256   if (after_executable)
257     *after_executable = first_argument;
258 }
259
260 /* Make sure @commandline is a valid UTF-16 string before
261  * calling this function!
262  * follow_class_chain_to_handler() does perform such validation.
263  */
264 static void
265 _g_win32_extract_executable (const gunichar2  *commandline,
266                              gchar           **ex_out,
267                              gchar           **ex_basename_out,
268                              gchar           **ex_folded_out,
269                              gchar           **ex_folded_basename_out,
270                              gchar           **dll_function_out)
271 {
272   gchar *ex;
273   gchar *ex_folded;
274   const gunichar2 *first_argument;
275   const gunichar2 *executable;
276   const gunichar2 *executable_basename;
277   gboolean quoted;
278   gboolean folded;
279   gssize execlen;
280
281   _g_win32_parse_filename (commandline, FALSE, &executable, &execlen, &executable_basename, &first_argument);
282
283   commandline = executable;
284
285   while ((wchar_t) first_argument[0] == L' ')
286     first_argument++;
287
288   folded = g_utf16_to_utf8_and_fold (executable, (gssize) execlen, &ex, &ex_folded);
289   /* This should never fail as @executable has to be valid UTF-16. */
290   g_assert (folded);
291
292   if (dll_function_out)
293     *dll_function_out = NULL;
294
295   /* See if the executable basename is "rundll32.exe". If so, then
296    * parse the rest of the commandline as r'"?path-to-dll"?[ ]*,*[ ]*dll_function_to_invoke'
297    */
298   /* Using just "rundll32.exe", without an absolute path, seems
299    * very exploitable, but MS does that sometimes, so we have
300    * to accept that.
301    */
302   if ((g_strcmp0 (ex_folded, "rundll32.exe") == 0 ||
303        g_str_has_suffix (ex_folded, "\\rundll32.exe") ||
304        g_str_has_suffix (ex_folded, "/rundll32.exe")) &&
305       first_argument[0] != 0 &&
306       dll_function_out != NULL)
307     {
308       /* Corner cases:
309        * > rundll32.exe c:\some,file,with,commas.dll,some_function
310        * is treated by rundll32 as:
311        * dll=c:\some
312        * function=file,with,commas.dll,some_function
313        * unless the dll name is surrounded by double quotation marks:
314        * > rundll32.exe "c:\some,file,with,commas.dll",some_function
315        * in which case everything works normally.
316        * Also, quoting only works if it surrounds the file name, i.e:
317        * > rundll32.exe "c:\some,file"",with,commas.dll",some_function
318        * will not work.
319        * Also, comma is optional when filename is quoted or when function
320        * name is separated from the filename by space(s):
321        * > rundll32.exe "c:\some,file,with,commas.dll"some_function
322        * will work,
323        * > rundll32.exe c:\some_dll_without_commas_or_spaces.dll some_function
324        * will work too.
325        * Also, any number of commas is accepted:
326        * > rundll32.exe c:\some_dll_without_commas_or_spaces.dll , , ,,, , some_function
327        * works just fine.
328        * And the ultimate example is:
329        * > "rundll32.exe""c:\some,file,with,commas.dll"some_function
330        * and it also works.
331        * Good job, Microsoft!
332        */
333       const gunichar2 *filename_end = NULL;
334       gssize filename_len = 0;
335       gssize function_len = 0;
336       const gunichar2 *dllpart;
337
338       quoted = FALSE;
339
340       if ((wchar_t) first_argument[0] == L'"')
341         quoted = TRUE;
342
343       _g_win32_parse_filename (first_argument, TRUE, &dllpart, &filename_len, NULL, &filename_end);
344
345       if (filename_end[0] != 0 && filename_len > 0)
346         {
347           const gunichar2 *function_begin = filename_end;
348
349           while ((wchar_t) function_begin[0] == L',' || (wchar_t) function_begin[0] == L' ')
350             function_begin += 1;
351
352           if (function_begin[0] != 0)
353             {
354               gchar *dllpart_utf8;
355               gchar *dllpart_utf8_folded;
356               gchar *function_utf8;
357               gboolean folded;
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 }