(root)/
glib-2.79.0/
gio/
giowin32-private.c
       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  }