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