[glib: 3/5] W32: significant symlink code changes



commit 62d387151dfd7c3f807370844d9a936d6d42699c
Author: Руслан Ижбулатов <lrn1986 gmail com>
Date:   Fri Aug 24 09:16:46 2018 +0000

    W32: significant symlink code changes
    
    Put the core readlink() code into a separate
    _g_win32_readlink_handle_raw() function that takes a file handle,
    can optionally ensure NUL-terminatedness of its output
    (for cases where we need a NUL-terminator and do *not* need
    to get the exact contents of the symlink as it is stored in FS)
    and can either fill a caller-provided buffer *or* allocate
    its own buffer, and can also read the reparse tag.
    
    Put the rest of readlink() code into separate
    functions that do UTF-16<->UTF-8, strip inconvenient prefix
    and open/close the symlink file handle as needed.
    
    Split _g_win32_stat_utf16_no_trailing_slashes() into
    two functions - the one that takes a filename and the one
    that takes a file descriptor. The part of these functions
    that would have been duplicate is now split into the
    _g_win32_fill_privatestat() funcion.
    
    Add more comments explaining what each function does.
    Only g_win32_readlink_utf8(), which is callable from outside
    via private function interface, gets a real doc-comment,
    the rest get normal, non-doc comments.
    
    Change all callers to use the new version of the private
    g_win32_readlink_utf8() function, which can now NUL-terminate
    and allocate on demand - no need to call it in a loop.
    
    Also, the new code should correctly get reparse tag when the
    caller does fstat() on a symlink. Do note that this requires
    the caller to get a FD for the symlink, not the target. Figuring
    out how to do that is up to the caller.
    
    Since symlink info (target path and reparse tag) are now always
    read directly, via DeviceIoControl(), we don't need to use
    FindFirstFileW() anymore.

 gio/glocalfileinfo.c   |  19 +-
 glib/gfileutils.c      |  27 +-
 glib/glib-private.h    |  20 +-
 glib/gstdio-private.c  |  89 ++++++
 glib/gstdio.c          | 805 ++++++++++++++++++++++++++++---------------------
 glib/gstdioprivate.h   |   8 +-
 glib/tests/fileutils.c | 228 ++++++++++++++
 7 files changed, 824 insertions(+), 372 deletions(-)
---
diff --git a/gio/glocalfileinfo.c b/gio/glocalfileinfo.c
index 1387dc077..ca2e9234b 100644
--- a/gio/glocalfileinfo.c
+++ b/gio/glocalfileinfo.c
@@ -161,7 +161,7 @@ _g_local_file_info_create_fs_id (GLocalFileStat *statbuf)
 static gchar *
 read_link (const gchar *full_name)
 {
-#if defined (HAVE_READLINK) || defined (G_OS_WIN32)
+#if defined (HAVE_READLINK)
   gchar *buffer;
   guint size;
   
@@ -171,12 +171,8 @@ read_link (const gchar *full_name)
   while (1)
     {
       int read_size;
-      
-#ifndef G_OS_WIN32
+
       read_size = readlink (full_name, buffer, size);
-#else
-      read_size = GLIB_PRIVATE_CALL (g_win32_readlink_utf8) (full_name, buffer, size);
-#endif
       if (read_size < 0)
        {
          g_free (buffer);
@@ -190,6 +186,17 @@ read_link (const gchar *full_name)
       size *= 2;
       buffer = g_realloc (buffer, size);
     }
+#elif defined (G_OS_WIN32)
+  gchar *buffer;
+  int read_size;
+
+  read_size = GLIB_PRIVATE_CALL (g_win32_readlink_utf8) (full_name, NULL, 0, &buffer, TRUE);
+  if (read_size < 0)
+    return NULL;
+  else if (read_size == 0)
+    return strdup ("");
+  else
+    return buffer;
 #else
   return NULL;
 #endif
diff --git a/glib/gfileutils.c b/glib/gfileutils.c
index 1e7a771a9..b39d3d478 100644
--- a/glib/gfileutils.c
+++ b/glib/gfileutils.c
@@ -2090,7 +2090,7 @@ gchar *
 g_file_read_link (const gchar  *filename,
                  GError      **error)
 {
-#if defined (HAVE_READLINK) || defined (G_OS_WIN32)
+#if defined (HAVE_READLINK)
   gchar *buffer;
   size_t size;
   gssize read_size;
@@ -2103,11 +2103,7 @@ g_file_read_link (const gchar  *filename,
   
   while (TRUE) 
     {
-#ifndef G_OS_WIN32
       read_size = readlink (filename, buffer, size);
-#else
-      read_size = g_win32_readlink_utf8 (filename, buffer, size);
-#endif
       if (read_size < 0)
         {
           int saved_errno = errno;
@@ -2128,6 +2124,27 @@ g_file_read_link (const gchar  *filename,
       size *= 2;
       buffer = g_realloc (buffer, size);
     }
+#elif defined (G_OS_WIN32)
+  gchar *buffer;
+  gssize read_size;
+  
+  g_return_val_if_fail (filename != NULL, NULL);
+  g_return_val_if_fail (error == NULL || *error == NULL, NULL);
+
+  read_size = g_win32_readlink_utf8 (filename, NULL, 0, &buffer, TRUE);
+  if (read_size < 0)
+    {
+      int saved_errno = errno;
+      set_file_error (error,
+                      filename,
+                      _("Failed to read the symbolic link “%s”: %s"),
+                      saved_errno);
+      return NULL;
+    }
+  else if (read_size == 0)
+    return strdup ("");
+  else
+    return buffer;
 #else
   g_return_val_if_fail (filename != NULL, NULL);
   g_return_val_if_fail (error == NULL || *error == NULL, NULL);
diff --git a/glib/glib-private.h b/glib/glib-private.h
index 31acd9386..facdbb26b 100644
--- a/glib/glib-private.h
+++ b/glib/glib-private.h
@@ -67,18 +67,20 @@ typedef struct {
 
   /* See gstdio.c */
 #ifdef G_OS_WIN32
-  int                   (* g_win32_stat_utf8)           (const gchar       *filename,
-                                                         GWin32PrivateStat *buf);
+  int                   (* g_win32_stat_utf8)           (const gchar        *filename,
+                                                         GWin32PrivateStat  *buf);
 
-  int                   (* g_win32_lstat_utf8)          (const gchar       *filename,
-                                                         GWin32PrivateStat *buf);
+  int                   (* g_win32_lstat_utf8)          (const gchar        *filename,
+                                                         GWin32PrivateStat  *buf);
 
-  int                   (* g_win32_readlink_utf8)       (const gchar *filename,
-                                                         gchar       *buf,
-                                                         gsize        buf_size);
+  int                   (* g_win32_readlink_utf8)       (const gchar        *filename,
+                                                         gchar              *buf,
+                                                         gsize               buf_size,
+                                                         gchar             **alloc_buf,
+                                                         gboolean            terminate);
 
-  int                   (* g_win32_fstat)               (int                fd,
-                                                         GWin32PrivateStat *buf);
+  int                   (* g_win32_fstat)               (int                 fd,
+                                                         GWin32PrivateStat  *buf);
 #endif
 
 
diff --git a/glib/gstdio-private.c b/glib/gstdio-private.c
index 5eaaf09c5..6d86ce48c 100644
--- a/glib/gstdio-private.c
+++ b/glib/gstdio-private.c
@@ -75,3 +75,92 @@ _g_win32_strip_extended_ntobjm_prefix (gunichar2 *str,
 
   return do_move;
 }
+
+static int
+_g_win32_copy_and_maybe_terminate (const guchar *data,
+                                   gsize         in_to_copy,
+                                   gunichar2    *buf,
+                                   gsize         buf_size,
+                                   gunichar2   **alloc_buf,
+                                   gboolean      terminate)
+{
+  gsize to_copy = in_to_copy;
+  /* Number of bytes we can use to add extra zeroes for NUL-termination.
+   * 0 means that we can destroy up to 2 bytes of data,
+   * 1 means that we can destroy up to 1 byte of data,
+   * 2 means that we do not perform destructive NUL-termination
+   */
+  gsize extra_bytes = terminate ? 2 : 0;
+  char *buf_in_chars;
+
+  if (to_copy == 0)
+    return 0;
+
+  /* 2 bytes is sizeof (wchar_t), for an extra NUL-terminator. */
+  if (buf)
+    {
+      if (to_copy >= buf_size)
+        {
+          extra_bytes = 0;
+          to_copy = buf_size;
+        }
+      else if (to_copy > buf_size - 2)
+        {
+          extra_bytes = 1;
+        }
+
+      memcpy (buf, data, to_copy);
+    }
+  else
+    {
+      /* Note that SubstituteNameLength is USHORT, so to_copy + 2, being
+       * gsize, never overflows.
+       */
+      *alloc_buf = g_malloc (to_copy + extra_bytes);
+      memcpy (*alloc_buf, data, to_copy);
+    }
+
+  if (!terminate)
+    return to_copy;
+
+  if (buf)
+    buf_in_chars = (char *) buf;
+  else
+    buf_in_chars = (char *) *alloc_buf;
+
+  if (to_copy >= 2 && buf_in_chars[to_copy - 2] == 0 &&
+      buf_in_chars[to_copy - 1] == 0)
+    {
+      /* Fully NUL-terminated, do nothing */
+    }
+  else if ((to_copy == 1 || buf_in_chars[to_copy - 2] != 0) &&
+           buf_in_chars[to_copy - 1] == 0)
+    {
+      /* Have one zero, try to add another one */
+      if (extra_bytes > 0)
+        {
+          /* Append trailing zero */
+          buf_in_chars[to_copy] = 0;
+          /* Be precise about the number of bytes we return */
+          to_copy += 1;
+        }
+      else if (to_copy >= 2)
+        {
+          /* No space for appending, destroy one byte */
+          buf_in_chars[to_copy - 2] = 0;
+        }
+      /* else there's no space at all (to_copy == 1), do nothing */
+    }
+  else if (extra_bytes > 0 || to_copy >= 2)
+    {
+      buf_in_chars[to_copy - 2 + extra_bytes] = 0;
+      buf_in_chars[to_copy - 1 + extra_bytes] = 0;
+      to_copy += extra_bytes;
+    }
+  else /* extra_bytes == 0 && to_copy == 1 */
+    {
+      buf_in_chars[0] = 0;
+    }
+
+  return to_copy;
+}
diff --git a/glib/gstdio.c b/glib/gstdio.c
index 4243fd5a7..51a1ddbe9 100644
--- a/glib/gstdio.c
+++ b/glib/gstdio.c
@@ -129,6 +129,8 @@ w32_error_to_errno (DWORD error_code)
  * FT = UT * 10000000 + 116444736000000000.
  * Therefore:
  * UT = (FT - 116444736000000000) / 10000000.
+ * Converts FILETIME to unix epoch time in form
+ * of a signed 64-bit integer (can be negative).
  */
 static gint64
 _g_win32_filetime_to_unix_time (FILETIME *ft)
@@ -165,6 +167,9 @@ _g_win32_filetime_to_unix_time (FILETIME *ft)
 #    endif
 #  endif
 
+/* Uses filename and BHFI to fill a stat64 structure.
+ * Tries to reproduce the behaviour and quirks of MS C runtime stat().
+ */
 static int
 _g_win32_fill_statbuf_from_handle_info (const wchar_t              *filename,
                                         const wchar_t              *filename_target,
@@ -258,67 +263,322 @@ _g_win32_fill_statbuf_from_handle_info (const wchar_t              *filename,
   return 0;
 }
 
+/* Fills our private stat-like structure using data from
+ * a normal stat64 struct, BHFI, FSI and a reparse tag.
+ */
+static void
+_g_win32_fill_privatestat (const struct __stat64            *statbuf,
+                           const BY_HANDLE_FILE_INFORMATION *handle_info,
+                           const FILE_STANDARD_INFO         *std_info,
+                           DWORD                             reparse_tag,
+                           GWin32PrivateStat                *buf)
+{
+  buf->st_dev = statbuf->st_dev;
+  buf->st_mode = statbuf->st_mode;
+  buf->volume_serial = handle_info->dwVolumeSerialNumber;
+  buf->file_index = (((guint64) handle_info->nFileIndexHigh) << 32) | handle_info->nFileIndexLow;
+  buf->attributes = handle_info->dwFileAttributes;
+  buf->st_nlink = handle_info->nNumberOfLinks;
+  buf->st_size = (((guint64) handle_info->nFileSizeHigh) << 32) | handle_info->nFileSizeLow;
+  buf->allocated_size = std_info->AllocationSize.QuadPart;
+
+  buf->reparse_tag = reparse_tag;
+
+  buf->st_ctime = statbuf->st_ctime;
+  buf->st_atime = statbuf->st_atime;
+  buf->st_mtime = statbuf->st_mtime;
+}
+
+/* Read the link data from a symlink/mountpoint represented
+ * by the handle. Also reads reparse tag.
+ * @reparse_tag receives the tag. Can be %NULL if @buf or @alloc_buf
+ *              is non-NULL.
+ * @buf receives the link data. Can be %NULL if reparse_tag is non-%NULL.
+ *      Mutually-exclusive with @alloc_buf.
+ * @buf_size is the size of the @buf, in bytes.
+ * @alloc_buf points to a location where internally-allocated buffer
+ *            pointer will be written. That buffer receives the
+ *            link data. Mutually-exclusive with @buf.
+ * @terminate ensures that the buffer is NUL-terminated if
+ *            it isn't already. Note that this can erase useful
+ *            data if @buf is provided and @buf_size is too small.
+ *            Specifically, with @buf_size <= 2 the buffer will
+ *            receive an empty string, even if there is some
+ *            data in the reparse point.
+ * The contents of @buf or @alloc_buf are presented as-is - could
+ * be non-NUL-terminated (unless @terminate is %TRUE) or even malformed.
+ * Returns the number of bytes (!) placed into @buf or @alloc_buf,
+ * including NUL-terminator (if any).
+ *
+ * Returned value of 0 means that there's no recognizable data in the
+ * reparse point. @alloc_buf will not be allocated in that case,
+ * and @buf will be left unmodified.
+ *
+ * If @buf and @alloc_buf are %NULL, returns 0 to indicate success.
+ * Returns -1 to indicate an error, sets errno.
+ */
+static int
+_g_win32_readlink_handle_raw (HANDLE      h,
+                              DWORD      *reparse_tag,
+                              gunichar2  *buf,
+                              gsize       buf_size,
+                              gunichar2 **alloc_buf,
+                              gboolean    terminate)
+{
+  DWORD error_code;
+  DWORD returned_bytes = 0;
+  BYTE *data;
+  gsize to_copy;
+  /* This is 16k. It's impossible to make DeviceIoControl() tell us
+   * the required size. NtFsControlFile() does have such a feature,
+   * but for some reason it doesn't work with CreateFile()-returned handles.
+   * The only alternative is to repeatedly call DeviceIoControl()
+   * with bigger and bigger buffers, until it succeeds.
+   * We choose to sacrifice stack space for speed.
+   */
+  BYTE max_buffer[sizeof (REPARSE_DATA_BUFFER) + MAXIMUM_REPARSE_DATA_BUFFER_SIZE] = {0,};
+  DWORD max_buffer_size = sizeof (REPARSE_DATA_BUFFER) + MAXIMUM_REPARSE_DATA_BUFFER_SIZE;
+  REPARSE_DATA_BUFFER *rep_buf;
+
+  g_return_val_if_fail ((buf != NULL || alloc_buf != NULL || reparse_tag != NULL) &&
+                        (buf == NULL || alloc_buf == NULL),
+                        -1);
+
+  if (!DeviceIoControl (h, FSCTL_GET_REPARSE_POINT, NULL, 0,
+                        max_buffer,
+                        max_buffer_size,
+                        &returned_bytes, NULL))
+    {
+      error_code = GetLastError ();
+      errno = w32_error_to_errno (error_code);
+      return -1;
+    }
+
+  rep_buf = (REPARSE_DATA_BUFFER *) max_buffer;
+
+  if (reparse_tag != NULL)
+    *reparse_tag = rep_buf->ReparseTag;
+
+  if (buf == NULL && alloc_buf == NULL)
+    return 0;
+
+  if (rep_buf->ReparseTag == IO_REPARSE_TAG_SYMLINK)
+    {
+      data = &((BYTE *) 
rep_buf->SymbolicLinkReparseBuffer.PathBuffer)[rep_buf->SymbolicLinkReparseBuffer.SubstituteNameOffset];
+
+      to_copy = rep_buf->SymbolicLinkReparseBuffer.SubstituteNameLength;
+    }
+  else if (rep_buf->ReparseTag == IO_REPARSE_TAG_MOUNT_POINT)
+    {
+      data = &((BYTE *) 
rep_buf->MountPointReparseBuffer.PathBuffer)[rep_buf->MountPointReparseBuffer.SubstituteNameOffset];
+
+      to_copy = rep_buf->MountPointReparseBuffer.SubstituteNameLength;
+    }
+  else
+    to_copy = 0;
+
+  return _g_win32_copy_and_maybe_terminate (data, to_copy, buf, buf_size, alloc_buf, terminate);
+}
+
+/* Read the link data from a symlink/mountpoint represented
+ * by the @filename.
+ * @filename is the name of the file.
+ * @reparse_tag receives the tag. Can be %NULL if @buf or @alloc_buf
+ *              is non-%NULL.
+ * @buf receives the link data. Mutually-exclusive with @alloc_buf.
+ * @buf_size is the size of the @buf, in bytes.
+ * @alloc_buf points to a location where internally-allocated buffer
+ *            pointer will be written. That buffer receives the
+ *            link data. Mutually-exclusive with @buf.
+ * @terminate ensures that the buffer is NUL-terminated if
+ *            it isn't already
+ * The contents of @buf or @alloc_buf are presented as-is - could
+ * be non-NUL-terminated (unless @terminate is TRUE) or even malformed.
+ * Returns the number of bytes (!) placed into @buf or @alloc_buf.
+ * Returned value of 0 means that there's no recognizable data in the
+ * reparse point. @alloc_buf will not be allocated in that case,
+ * and @buf will be left unmodified.
+ * If @buf and @alloc_buf are %NULL, returns 0 to indicate success.
+ * Returns -1 to indicate an error, sets errno.
+ */
+static int
+_g_win32_readlink_utf16_raw (const gunichar2  *filename,
+                             DWORD            *reparse_tag,
+                             gunichar2        *buf,
+                             gsize             buf_size,
+                             gunichar2       **alloc_buf,
+                             gboolean          terminate)
+{
+  HANDLE h;
+  DWORD attributes;
+  DWORD to_copy;
+  DWORD error_code;
+
+  if ((attributes = GetFileAttributesW (filename)) == 0)
+    {
+      error_code = GetLastError ();
+      errno = w32_error_to_errno (error_code);
+      return -1;
+    }
+
+  if ((attributes & FILE_ATTRIBUTE_REPARSE_POINT) == 0)
+    {
+      errno = EINVAL;
+      return -1;
+    }
+
+  /* To read symlink target we need to open the file as a reparse
+   * point and use DeviceIoControl() on it.
+   */
+  h = CreateFileW (filename,
+                   FILE_READ_EA,
+                   FILE_SHARE_READ|FILE_SHARE_WRITE|FILE_SHARE_DELETE,
+                   NULL, OPEN_EXISTING,
+                   FILE_ATTRIBUTE_NORMAL
+                   | FILE_FLAG_OPEN_REPARSE_POINT
+                   | (attributes & FILE_ATTRIBUTE_DIRECTORY ? FILE_FLAG_BACKUP_SEMANTICS : 0),
+                   NULL);
+
+  if (h == INVALID_HANDLE_VALUE)
+    {
+      error_code = GetLastError ();
+      errno = w32_error_to_errno (error_code);
+      return -1;
+    }
+
+  to_copy = _g_win32_readlink_handle_raw (h, reparse_tag, buf, buf_size, alloc_buf, terminate);
+
+  CloseHandle (h);
+
+  return to_copy;
+}
+
+/* Read the link data from a symlink/mountpoint represented
+ * by a UTF-16 filename or a file handle.
+ * @filename is the name of the file. Mutually-exclusive with @file_handle.
+ * @file_handle is the handle of the file. Mutually-exclusive with @filename.
+ * @reparse_tag receives the tag. Can be %NULL if @buf or @alloc_buf
+ *              is non-%NULL.
+ * @buf receives the link data. Mutually-exclusive with @alloc_buf.
+ * @buf_size is the size of the @buf, in bytes.
+ * @alloc_buf points to a location where internally-allocated buffer
+ *            pointer will be written. That buffer receives the
+ *            link data. Mutually-exclusive with @buf.
+ * @terminate ensures that the buffer is NUL-terminated if
+ *            it isn't already
+ * The contents of @buf or @alloc_buf are adjusted
+ * (extended or nt object manager prefix is stripped),
+ * but otherwise they are presented as-is - could be non-NUL-terminated
+ * (unless @terminate is TRUE) or even malformed.
+ * Returns the number of bytes (!) placed into @buf or @alloc_buf.
+ * Returned value of 0 means that there's no recognizable data in the
+ * reparse point. @alloc_buf will not be allocated in that case,
+ * and @buf will be left unmodified.
+ * Returns -1 to indicate an error, sets errno.
+ */
+static int
+_g_win32_readlink_utf16_handle (const gunichar2  *filename,
+                                HANDLE            file_handle,
+                                DWORD            *reparse_tag,
+                                gunichar2        *buf,
+                                gsize             buf_size,
+                                gunichar2       **alloc_buf,
+                                gboolean          terminate)
+{
+  int   result;
+  gsize string_size;
+
+  g_return_val_if_fail ((buf != NULL || alloc_buf != NULL || reparse_tag != NULL) &&
+                        (filename != NULL || file_handle != NULL) &&
+                        (buf == NULL || alloc_buf == NULL) &&
+                        (filename == NULL || file_handle == NULL),
+                        -1);
+
+  if (filename)
+    result = _g_win32_readlink_utf16_raw (filename, reparse_tag, buf, buf_size, alloc_buf, terminate);
+  else
+    result = _g_win32_readlink_handle_raw (file_handle, reparse_tag, buf, buf_size, alloc_buf, terminate);
+
+  if (result <= 0)
+    return result;
+
+  /* Ensure that output is a multiple of sizeof (gunichar2),
+   * cutting any trailing partial gunichar2, if present.
+   */
+  result -= result % sizeof (gunichar2);
+
+  if (result <= 0)
+    return result;
+
+  /* DeviceIoControl () tends to return filenames as NT Object Manager
+   * names , i.e. "\\??\\C:\\foo\\bar".
+   * Remove the leading 4-byte "\\??\\" prefix, as glib (as well as many W32 API
+   * functions) is unprepared to deal with it. Unless it has no 'x:' drive
+   * letter part after the prefix, in which case we leave everything
+   * as-is, because the path could be "\\??\\Volume{GUID}" - stripping
+   * the prefix will allow it to be confused with relative links
+   * targeting "Volume{GUID}".
+   */
+  string_size = result / sizeof (gunichar2);
+  _g_win32_strip_extended_ntobjm_prefix (buf ? buf : *alloc_buf, &string_size);
+
+  return string_size * sizeof (gunichar2);
+}
+
+/* Works like stat() or lstat(), depending on the value of @for_symlink,
+ * but accepts filename in UTF-16 and fills our custom stat structure.
+ * The @filename must not have trailing slashes.
+ */
 static int
 _g_win32_stat_utf16_no_trailing_slashes (const gunichar2    *filename,
-                                         int                 fd,
                                          GWin32PrivateStat  *buf,
                                          gboolean            for_symlink)
 {
-  HANDLE file_handle;
-  gboolean succeeded_so_far;
-  DWORD error_code;
   struct __stat64 statbuf;
   BY_HANDLE_FILE_INFORMATION handle_info;
   FILE_STANDARD_INFO std_info;
-  WIN32_FIND_DATAW finddata;
-  DWORD immediate_attributes;
   gboolean is_symlink = FALSE;
-  gboolean is_directory;
-  DWORD open_flags;
   wchar_t *filename_target = NULL;
-  int result;
+  DWORD immediate_attributes;
+  DWORD open_flags;
+  gboolean is_directory;
+  DWORD reparse_tag = 0;
+  DWORD error_code;
+  BOOL succeeded_so_far;
+  HANDLE file_handle;
 
-  if (fd < 0)
-    {
-      immediate_attributes = GetFileAttributesW (filename);
+  immediate_attributes = GetFileAttributesW (filename);
 
-      if (immediate_attributes == INVALID_FILE_ATTRIBUTES)
-        {
-          error_code = GetLastError ();
-          errno = w32_error_to_errno (error_code);
+  if (immediate_attributes == INVALID_FILE_ATTRIBUTES)
+    {
+      error_code = GetLastError ();
+      errno = w32_error_to_errno (error_code);
 
-          return -1;
-        }
+      return -1;
+    }
 
-      is_symlink = (immediate_attributes & FILE_ATTRIBUTE_REPARSE_POINT) == FILE_ATTRIBUTE_REPARSE_POINT;
-      is_directory = (immediate_attributes & FILE_ATTRIBUTE_DIRECTORY) == FILE_ATTRIBUTE_DIRECTORY;
+  is_symlink = (immediate_attributes & FILE_ATTRIBUTE_REPARSE_POINT) == FILE_ATTRIBUTE_REPARSE_POINT;
+  is_directory = (immediate_attributes & FILE_ATTRIBUTE_DIRECTORY) == FILE_ATTRIBUTE_DIRECTORY;
 
-      open_flags = FILE_ATTRIBUTE_NORMAL;
+  open_flags = FILE_ATTRIBUTE_NORMAL;
 
-      if (for_symlink && is_symlink)
-        open_flags |= FILE_FLAG_OPEN_REPARSE_POINT;
+  if (for_symlink && is_symlink)
+    open_flags |= FILE_FLAG_OPEN_REPARSE_POINT;
 
-      if (is_directory)
-        open_flags |= FILE_FLAG_BACKUP_SEMANTICS;
+  if (is_directory)
+    open_flags |= FILE_FLAG_BACKUP_SEMANTICS;
 
-      file_handle = CreateFileW (filename, FILE_READ_ATTRIBUTES,
-                                 FILE_SHARE_READ, NULL, OPEN_EXISTING,
-                                 open_flags,
-                                 NULL);
+  file_handle = CreateFileW (filename, FILE_READ_ATTRIBUTES | FILE_READ_EA,
+                             FILE_SHARE_READ|FILE_SHARE_WRITE|FILE_SHARE_DELETE,
+                             NULL, OPEN_EXISTING,
+                             open_flags,
+                             NULL);
 
-      if (file_handle == INVALID_HANDLE_VALUE)
-        {
-          error_code = GetLastError ();
-          errno = w32_error_to_errno (error_code);
-          return -1;
-        }
-    }
-  else
+  if (file_handle == INVALID_HANDLE_VALUE)
     {
-      file_handle = (HANDLE) _get_osfhandle (fd);
-
-      if (file_handle == INVALID_HANDLE_VALUE)
-        return -1;
+      error_code = GetLastError ();
+      errno = w32_error_to_errno (error_code);
+      return -1;
     }
 
   succeeded_so_far = GetFileInformationByHandle (file_handle,
@@ -336,182 +596,104 @@ _g_win32_stat_utf16_no_trailing_slashes (const gunichar2    *filename,
 
   if (!succeeded_so_far)
     {
-      if (fd < 0)
-        CloseHandle (file_handle);
+      CloseHandle (file_handle);
       errno = w32_error_to_errno (error_code);
       return -1;
     }
 
   /* It's tempting to use GetFileInformationByHandleEx(FileAttributeTagInfo),
    * but it always reports that the ReparseTag is 0.
+   * We already have a handle open for symlink, use that.
+   * For the target we have to specify a filename, and the function
+   * will open another handle internally.
    */
-  if (fd < 0)
+  if (is_symlink &&
+      _g_win32_readlink_utf16_handle (for_symlink ? NULL : filename,
+                                      for_symlink ? file_handle : NULL,
+                                      &reparse_tag,
+                                      NULL, 0,
+                                      for_symlink ? NULL : &filename_target,
+                                      TRUE) < 0)
     {
-      memset (&finddata, 0, sizeof (finddata));
+      CloseHandle (file_handle);
+      return -1;
+    }
 
-      if (handle_info.dwFileAttributes & FILE_ATTRIBUTE_REPARSE_POINT)
-        {
-          HANDLE tmp = FindFirstFileW (filename,
-                                       &finddata);
-
-          if (tmp == INVALID_HANDLE_VALUE)
-            {
-              error_code = GetLastError ();
-              errno = w32_error_to_errno (error_code);
-              CloseHandle (file_handle);
-              return -1;
-            }
-
-          FindClose (tmp);
-        }
+  CloseHandle (file_handle);
 
-      if (is_symlink && !for_symlink)
-        {
-          /* If filename is a symlink, but we need the target.
-           * To get information about the target we need to resolve
-           * the symlink first.
-           */
-          DWORD filename_target_len;
-          DWORD new_len;
-
-          /* Just in case, give it a real memory location instead of NULL */
-          new_len = GetFinalPathNameByHandleW (file_handle,
-                                               (wchar_t *) &filename_target_len,
-                                               0,
-                                               FILE_NAME_NORMALIZED);
-
-#define SANE_LIMIT 1024 * 10
-          if (new_len >= SANE_LIMIT)
-#undef SANE_LIMIT
-            {
-              new_len = 0;
-              error_code = ERROR_BUFFER_OVERFLOW;
-            }
-          else if (new_len == 0)
-            {
-              error_code = GetLastError ();
-            }
-
-          if (new_len > 0)
-            {
-              /* Pretend that new_len doesn't count the terminating NUL char,
-               * and ask for a bit more space than is needed, and allocate even more.
-               */
-              filename_target_len = new_len + 3;
-              filename_target = g_malloc ((filename_target_len + 1) * sizeof (wchar_t));
-
-              new_len = GetFinalPathNameByHandleW (file_handle,
-                                                   filename_target,
-                                                   filename_target_len,
-                                                   FILE_NAME_NORMALIZED);
-
-              /* filename_target_len is already larger than needed,
-               * new_len should be smaller than that, even if the size
-               * is off by 1 for some reason.
-               */
-              if (new_len >= filename_target_len - 1)
-                {
-                  new_len = 0;
-                  error_code = ERROR_BUFFER_OVERFLOW;
-                  g_clear_pointer (&filename_target, g_free);
-                }
-              else if (new_len == 0)
-                {
-                  g_clear_pointer (&filename_target, g_free);
-                }
-              /* GetFinalPathNameByHandle() is documented to return extended paths,
-               * strip the extended prefix, if it is followed by a drive letter
-               * and a colon. Otherwise keep it (the path could be
-               * \\\\?\\Volume{GUID}\\ - it's only usable in extended form).
-               */
-              else if (new_len > 0)
-                {
-                  gsize len = new_len;
-
-                  /* Account for NUL-terminator maybe not being counted.
-                   * This is why we overallocated earlier.
-                   */
-                  if (filename_target[len] != L'\0')
-                    {
-                      len++;
-                      filename_target[len] = L'\0';
-                    }
-
-                  _g_win32_strip_extended_ntobjm_prefix (filename_target, &len);
-                  new_len = len;
-                }
-
-            }
-
-          if (new_len == 0)
-            succeeded_so_far = FALSE;
-        }
+  _g_win32_fill_statbuf_from_handle_info (filename,
+                                          filename_target,
+                                          &handle_info,
+                                          &statbuf);
+  g_free (filename_target);
+  _g_win32_fill_privatestat (&statbuf,
+                             &handle_info,
+                             &std_info,
+                             reparse_tag,
+                             buf);
 
-      CloseHandle (file_handle);
-    }
-  /* else if fd >= 0 the file_handle was obtained via _get_osfhandle()
-   * and must not be closed, it is owned by fd.
-   */
+  return 0;
+}
 
-  if (!succeeded_so_far)
-    {
-      errno = w32_error_to_errno (error_code);
-      return -1;
-    }
+/* Works like fstat(), but fills our custom stat structure. */
+static int
+_g_win32_stat_fd (int                 fd,
+                  GWin32PrivateStat  *buf)
+{
+  HANDLE file_handle;
+  gboolean succeeded_so_far;
+  DWORD error_code;
+  struct __stat64 statbuf;
+  BY_HANDLE_FILE_INFORMATION handle_info;
+  FILE_STANDARD_INFO std_info;
+  DWORD reparse_tag = 0;
+  gboolean is_symlink = FALSE;
 
-  /*
-   * We can't use _wstat64() here, because with UCRT it now gives
-   * information about the target, even if we want information about
-   * the link itself (unlike MSVCRT, which gave information about
-   * the link, and if we needed information about the target we were
-   * able to resolve it by ourselves prior to calling _wstat64()).
-   */
-  if (fd < 0)
-    result = _g_win32_fill_statbuf_from_handle_info (filename,
-                                                     filename_target,
-                                                     &handle_info,
-                                                     &statbuf);
-  else
-    result = _fstat64 (fd, &statbuf);
+  file_handle = (HANDLE) _get_osfhandle (fd);
 
-  if (result != 0)
-    {
-      int errsv = errno;
+  if (file_handle == INVALID_HANDLE_VALUE)
+    return -1;
 
-      g_free (filename_target);
-      errno = errsv;
+  succeeded_so_far = GetFileInformationByHandle (file_handle,
+                                                 &handle_info);
+  error_code = GetLastError ();
+
+  if (succeeded_so_far)
+    {
+      succeeded_so_far = GetFileInformationByHandleEx (file_handle,
+                                                       FileStandardInfo,
+                                                       &std_info,
+                                                       sizeof (std_info));
+      error_code = GetLastError ();
+    }
 
+  if (!succeeded_so_far)
+    {
+      errno = w32_error_to_errno (error_code);
       return -1;
     }
 
-  g_free (filename_target);
+  is_symlink = (handle_info.dwFileAttributes & FILE_ATTRIBUTE_REPARSE_POINT) == FILE_ATTRIBUTE_REPARSE_POINT;
 
-  buf->st_dev = statbuf.st_dev;
-  buf->st_mode = statbuf.st_mode;
-  buf->volume_serial = handle_info.dwVolumeSerialNumber;
-  buf->file_index = (((guint64) handle_info.nFileIndexHigh) << 32) | handle_info.nFileIndexLow;
-  /* Note that immediate_attributes is for the symlink
-   * (if it's a symlink), while handle_info contains info
-   * about the symlink or the target, depending on the flags
-   * we used earlier.
-   */
-  buf->attributes = handle_info.dwFileAttributes;
-  buf->st_nlink = handle_info.nNumberOfLinks;
-  buf->st_size = (((guint64) handle_info.nFileSizeHigh) << 32) | handle_info.nFileSizeLow;
-  buf->allocated_size = std_info.AllocationSize.QuadPart;
+  if (is_symlink &&
+      _g_win32_readlink_handle_raw (file_handle, &reparse_tag, NULL, 0, NULL, FALSE) < 0)
+    return -1;
 
-  if (fd < 0 && buf->attributes & FILE_ATTRIBUTE_REPARSE_POINT)
-    buf->reparse_tag = finddata.dwReserved0;
-  else
-    buf->reparse_tag = 0;
+  if (_fstat64 (fd, &statbuf) != 0)
+    return -1;
 
-  buf->st_ctime = statbuf.st_ctime;
-  buf->st_atime = statbuf.st_atime;
-  buf->st_mtime = statbuf.st_mtime;
+  _g_win32_fill_privatestat (&statbuf,
+                             &handle_info,
+                             &std_info,
+                             reparse_tag,
+                             buf);
 
   return 0;
 }
 
+/* Works like stat() or lstat(), depending on the value of @for_symlink,
+ * but accepts filename in UTF-8 and fills our custom stat structure.
+ */
 static int
 _g_win32_stat_utf8 (const gchar       *filename,
                     GWin32PrivateStat *buf,
@@ -544,13 +726,16 @@ _g_win32_stat_utf8 (const gchar       *filename,
       return -1;
     }
 
-  result = _g_win32_stat_utf16_no_trailing_slashes (wfilename, -1, buf, for_symlink);
+  result = _g_win32_stat_utf16_no_trailing_slashes (wfilename, buf, for_symlink);
 
   g_free (wfilename);
 
   return result;
 }
 
+/* Works like stat(), but accepts filename in UTF-8
+ * and fills our custom stat structure.
+ */
 int
 g_win32_stat_utf8 (const gchar       *filename,
                    GWin32PrivateStat *buf)
@@ -558,6 +743,9 @@ g_win32_stat_utf8 (const gchar       *filename,
   return _g_win32_stat_utf8 (filename, buf, FALSE);
 }
 
+/* Works like lstat(), but accepts filename in UTF-8
+ * and fills our custom stat structure.
+ */
 int
 g_win32_lstat_utf8 (const gchar       *filename,
                     GWin32PrivateStat *buf)
@@ -565,138 +753,14 @@ g_win32_lstat_utf8 (const gchar       *filename,
   return _g_win32_stat_utf8 (filename, buf, TRUE);
 }
 
+/* Works like fstat(), but accepts filename in UTF-8
+ * and fills our custom stat structure.
+ */
 int
 g_win32_fstat (int                fd,
                GWin32PrivateStat *buf)
 {
-  return _g_win32_stat_utf16_no_trailing_slashes (NULL, fd, buf, FALSE);
-}
-
-static int
-_g_win32_readlink_utf16_raw (const gunichar2 *filename,
-                             gunichar2       *buf,
-                             gsize            buf_size)
-{
-  DWORD returned_bytes;
-  BYTE returned_data[MAXIMUM_REPARSE_DATA_BUFFER_SIZE]; /* This is 16k, by the way */
-  HANDLE h;
-  DWORD attributes;
-  REPARSE_DATA_BUFFER *rep_buf;
-  DWORD to_copy;
-  DWORD error_code;
-
-  if (buf_size > G_MAXSIZE / sizeof (wchar_t))
-    {
-      /* "buf_size * sizeof (wchar_t)" overflows */
-      errno = EFAULT;
-      return -1;
-    }
-
-  if ((attributes = GetFileAttributesW (filename)) == 0)
-    {
-      error_code = GetLastError ();
-      errno = w32_error_to_errno (error_code);
-      return -1;
-    }
-
-  if ((attributes & FILE_ATTRIBUTE_REPARSE_POINT) == 0)
-    {
-      errno = EINVAL;
-      return -1;
-    }
-
-  /* To read symlink target we need to open the file as a reparse
-   * point and use DeviceIoControl() on it.
-   */
-  h = CreateFileW (filename,
-                   FILE_READ_EA,
-                   FILE_SHARE_READ|FILE_SHARE_WRITE|FILE_SHARE_DELETE,
-                   NULL, OPEN_EXISTING,
-                   FILE_ATTRIBUTE_NORMAL
-                   | FILE_FLAG_OPEN_REPARSE_POINT
-                   | (attributes & FILE_ATTRIBUTE_DIRECTORY ? FILE_FLAG_BACKUP_SEMANTICS : 0),
-                   NULL);
-
-  if (h == INVALID_HANDLE_VALUE)
-    {
-      error_code = GetLastError ();
-      errno = w32_error_to_errno (error_code);
-      return -1;
-    }
-
-  if (!DeviceIoControl (h, FSCTL_GET_REPARSE_POINT, NULL, 0,
-                        returned_data, MAXIMUM_REPARSE_DATA_BUFFER_SIZE,
-                        &returned_bytes, NULL))
-    {
-      error_code = GetLastError ();
-      errno = w32_error_to_errno (error_code);
-      CloseHandle (h);
-      return -1;
-    }
-
-  rep_buf = (REPARSE_DATA_BUFFER *) returned_data;
-  to_copy = 0;
-
-  if (rep_buf->ReparseTag == IO_REPARSE_TAG_SYMLINK)
-    {
-      to_copy = rep_buf->SymbolicLinkReparseBuffer.SubstituteNameLength;
-
-      if (to_copy > buf_size * sizeof (wchar_t))
-        to_copy = buf_size * sizeof (wchar_t);
-
-      memcpy (buf,
-              &((BYTE *) 
rep_buf->SymbolicLinkReparseBuffer.PathBuffer)[rep_buf->SymbolicLinkReparseBuffer.SubstituteNameOffset],
-              to_copy);
-    }
-  else if (rep_buf->ReparseTag == IO_REPARSE_TAG_MOUNT_POINT)
-    {
-      to_copy = rep_buf->MountPointReparseBuffer.SubstituteNameLength;
-
-      if (to_copy > buf_size * sizeof (wchar_t))
-        to_copy = buf_size * sizeof (wchar_t);
-
-      memcpy (buf,
-              &((BYTE *) 
rep_buf->MountPointReparseBuffer.PathBuffer)[rep_buf->MountPointReparseBuffer.SubstituteNameOffset],
-              to_copy);
-    }
-
-  CloseHandle (h);
-
-  return to_copy;
-}
-
-static int
-_g_win32_readlink_utf16 (const gunichar2 *filename,
-                         gunichar2       *buf,
-                         gsize            buf_size)
-{
-  int   result = _g_win32_readlink_utf16_raw (filename, buf, buf_size);
-  gsize string_size;
-
-  if (result <= 0)
-    return result;
-
-  /* Ensure that output is a multiple of sizeof (gunichar2),
-   * cutting any trailing partial gunichar2, if present.
-   */
-  result -= result % sizeof (gunichar2);
-
-  if (result <= 0)
-    return result;
-
-  /* DeviceIoControl () tends to return filenames as NT Object Manager
-   * names , i.e. "\\??\\C:\\foo\\bar".
-   * Remove the leading 4-byte \??\ prefix, as glib (as well as many W32 API
-   * functions) is unprepared to deal with it. Unless it has no 'x:' drive
-   * letter part after the prefix, in which case we leave everything
-   * as-is, because the path could be "\??\Volume{GUID}" - stripping
-   * the prefix will allow it to be confused with relative links
-   * targeting "Volume{GUID}".
-   */
-  string_size = result / sizeof (gunichar2);
-  _g_win32_strip_extended_ntobjm_prefix (buf, &string_size);
-
-  return string_size * sizeof (gunichar2);
+  return _g_win32_stat_fd (fd, buf);
 }
 
 static gchar *
@@ -719,13 +783,54 @@ _g_win32_get_mode_alias (const gchar *mode)
   return alias;
 }
 
+/**
+ * g_win32_readlink_utf8:
+ * @filename: (type filename): a pathname in UTF-8
+ * @buf: (array length=buf_size) : a buffer to receive the reparse point
+ *                                 target path. Mutually-exclusive
+ *                                 with @alloc_buf.
+ * @buf_size: size of the @buf, in bytes
+ * @alloc_buf: points to a location where internally-allocated buffer
+ *             pointer will be written. That buffer receives the
+ *             link data. Mutually-exclusive with @buf.
+ * @terminate: ensures that the buffer is NUL-terminated if
+ *             it isn't already. If %FALSE, the returned string
+ *             might not be NUL-terminated (depends entirely on
+ *             what the contents of the filesystem are).
+ *
+ * Tries to read the reparse point indicated by @filename, filling
+ * @buf or @alloc_buf with the path that the reparse point redirects to.
+ * The path will be UTF-8-encoded, and an extended path prefix
+ * or a NT object manager prefix will be removed from it, if
+ * possible, but otherwise the path is returned as-is. Specifically,
+ * it could be a "\\\\Volume{GUID}\\" path. It also might use
+ * backslashes as path separators.
+ *
+ * Returns: -1 on error (sets errno), 0 if there's no (recognizable)
+ * path in the reparse point (@alloc_buf will not be allocated in that case,
+ * and @buf will be left unmodified),
+ * or the number of bytes placed into @buf otherwise,
+ * including NUL-terminator (if present or if @terminate is TRUE).
+ * The buffer returned via @alloc_buf should be freed with g_free().
+ *
+ * Since: 2.60
+ */
 int
-g_win32_readlink_utf8 (const gchar *filename,
-                       gchar       *buf,
-                       gsize        buf_size)
+g_win32_readlink_utf8 (const gchar  *filename,
+                       gchar        *buf,
+                       gsize         buf_size,
+                       gchar       **alloc_buf,
+                       gboolean      terminate)
 {
   wchar_t *wfilename;
   int result;
+  wchar_t *buf_utf16;
+  glong tmp_len;
+  gchar *tmp;
+
+  g_return_val_if_fail ((buf != NULL || alloc_buf != NULL) &&
+                        (buf == NULL || alloc_buf == NULL),
+                        -1);
 
   wfilename = g_utf8_to_utf16 (filename, -1, NULL, NULL, NULL);
 
@@ -735,39 +840,41 @@ g_win32_readlink_utf8 (const gchar *filename,
       return -1;
     }
 
-  result = _g_win32_readlink_utf16 (wfilename, (gunichar2 *) buf, buf_size);
+  result = _g_win32_readlink_utf16_handle (wfilename, NULL, NULL,
+                                           NULL, 0, &buf_utf16, terminate);
 
   g_free (wfilename);
 
-  if (result > 0)
-    {
-      glong tmp_len;
-      gchar *tmp = g_utf16_to_utf8 ((const gunichar2 *) buf,
-                                    result / sizeof (gunichar2),
-                                    NULL,
-                                    &tmp_len,
-                                    NULL);
-
-      if (tmp == NULL)
-        {
-          errno = EINVAL;
-          return -1;
-        }
+  if (result <= 0)
+    return result;
 
-      if (tmp_len > buf_size - 1)
-        tmp_len = buf_size - 1;
+  tmp = g_utf16_to_utf8 (buf_utf16,
+                         result / sizeof (gunichar2),
+                         NULL,
+                         &tmp_len,
+                         NULL);
 
-      memcpy (buf, tmp, tmp_len);
-      /* readlink() doesn't NUL-terminate, but we do.
-       * To be compliant, however, we return the
-       * number of bytes without the NUL-terminator.
-       */
-      buf[tmp_len] = '\0';
-      result = tmp_len;
-      g_free (tmp);
+  g_free (buf_utf16);
+
+  if (tmp == NULL)
+    {
+      errno = EINVAL;
+      return -1;
     }
 
-  return result;
+  if (alloc_buf)
+    {
+      *alloc_buf = tmp;
+      return tmp_len;
+    }
+
+  if (tmp_len > buf_size)
+    tmp_len = buf_size;
+
+  memcpy (buf, tmp, tmp_len);
+  g_free (tmp);
+
+  return tmp_len;
 }
 
 #endif
diff --git a/glib/gstdioprivate.h b/glib/gstdioprivate.h
index a100abb5e..71565388a 100644
--- a/glib/gstdioprivate.h
+++ b/glib/gstdioprivate.h
@@ -51,9 +51,11 @@ int g_win32_stat_utf8     (const gchar       *filename,
 int g_win32_lstat_utf8    (const gchar       *filename,
                            GWin32PrivateStat *buf);
 
-int g_win32_readlink_utf8 (const gchar *filename,
-                           gchar       *buf,
-                           gsize        buf_size);
+int g_win32_readlink_utf8 (const gchar       *filename,
+                           gchar             *buf,
+                           gsize              buf_size,
+                           gchar            **alloc_buf,
+                           gboolean           terminate);
 
 int g_win32_fstat         (int                fd,
                            GWin32PrivateStat *buf);
diff --git a/glib/tests/fileutils.c b/glib/tests/fileutils.c
index 3ef6615d0..adc735d79 100644
--- a/glib/tests/fileutils.c
+++ b/glib/tests/fileutils.c
@@ -1152,6 +1152,233 @@ test_win32_pathstrip (void)
     }
 }
 
+#define g_assert_memcmp(m1, cmp, m2, memlen, m1hex, m2hex, testcase_num) \
+G_STMT_START { \
+  if (memcmp (m1, m2, memlen) cmp 0); else \
+    g_assertion_message_cmpstr (G_LOG_DOMAIN, __FILE__, __LINE__, G_STRFUNC, \
+                                #m1hex " " #cmp " " #m2hex, m1hex, #cmp, m2hex); \
+} G_STMT_END
+
+static gchar *
+to_hex (const guchar *buf,
+        gsize        len)
+{
+  gsize i;
+  GString *s = g_string_new (NULL);
+  if (len > 0)
+    g_string_append_printf (s, "%02x", buf[0]);
+  for (i = 1; i < len; i++)
+    g_string_append_printf (s, " %02x", buf[i]);
+  return g_string_free (s, FALSE);
+}
+
+static void
+test_win32_zero_terminate_symlink (void)
+{
+  gsize i;
+#define TESTCASE(data, len_mod, use_buf, buf_size, terminate, reported_len, returned_string) \
+ { (const guchar *) data, wcslen (data) * 2 + len_mod, use_buf, buf_size, terminate, reported_len, (guchar 
*) returned_string},
+
+  struct
+  {
+    const guchar *data;
+    gsize         data_size;
+    gboolean      use_buf;
+    gsize         buf_size;
+    gboolean      terminate;
+    int           reported_len;
+    const guchar *returned_string;
+  } testcases[] = {
+    TESTCASE (L"foobar", +2, TRUE, 12 + 4, FALSE, 12 + 2, "f\0o\0o\0b\0a\0r\0\0\0")
+    TESTCASE (L"foobar", +2, TRUE, 12 + 3, FALSE, 12 + 2, "f\0o\0o\0b\0a\0r\0\0\0")
+    TESTCASE (L"foobar", +2, TRUE, 12 + 2, FALSE, 12 + 2, "f\0o\0o\0b\0a\0r\0\0\0")
+    TESTCASE (L"foobar", +2, TRUE, 12 + 1, FALSE, 12 + 1, "f\0o\0o\0b\0a\0r\0\0")
+    TESTCASE (L"foobar", +2, TRUE, 12 + 0, FALSE, 12 + 0, "f\0o\0o\0b\0a\0r\0")
+    TESTCASE (L"foobar", +2, TRUE, 12 - 1, FALSE, 12 - 1, "f\0o\0o\0b\0a\0r")
+    TESTCASE (L"foobar", +2, TRUE, 12 - 2, FALSE, 12 - 2, "f\0o\0o\0b\0a\0")
+    TESTCASE (L"foobar", +2, TRUE, 12 - 3, FALSE, 12 - 3, "f\0o\0o\0b\0a")
+    TESTCASE (L"foobar", +1, TRUE, 12 + 4, FALSE, 12 + 1, "f\0o\0o\0b\0a\0r\0\0")
+    TESTCASE (L"foobar", +1, TRUE, 12 + 3, FALSE, 12 + 1, "f\0o\0o\0b\0a\0r\0\0")
+    TESTCASE (L"foobar", +1, TRUE, 12 + 2, FALSE, 12 + 1, "f\0o\0o\0b\0a\0r\0\0")
+    TESTCASE (L"foobar", +1, TRUE, 12 + 1, FALSE, 12 + 1, "f\0o\0o\0b\0a\0r\0\0")
+    TESTCASE (L"foobar", +1, TRUE, 12 + 0, FALSE, 12 + 0, "f\0o\0o\0b\0a\0r\0")
+    TESTCASE (L"foobar", +1, TRUE, 12 - 1, FALSE, 12 - 1, "f\0o\0o\0b\0a\0r")
+    TESTCASE (L"foobar", +1, TRUE, 12 - 2, FALSE, 12 - 2, "f\0o\0o\0b\0a\0")
+    TESTCASE (L"foobar", +1, TRUE, 12 - 3, FALSE, 12 - 3, "f\0o\0o\0b\0a")
+    TESTCASE (L"foobar", +0, TRUE, 12 + 4, FALSE, 12 + 0, "f\0o\0o\0b\0a\0r\0")
+    TESTCASE (L"foobar", +0, TRUE, 12 + 3, FALSE, 12 + 0, "f\0o\0o\0b\0a\0r\0")
+    TESTCASE (L"foobar", +0, TRUE, 12 + 2, FALSE, 12 + 0, "f\0o\0o\0b\0a\0r\0")
+    TESTCASE (L"foobar", +0, TRUE, 12 + 1, FALSE, 12 + 0, "f\0o\0o\0b\0a\0r\0")
+    TESTCASE (L"foobar", +0, TRUE, 12 + 0, FALSE, 12 + 0, "f\0o\0o\0b\0a\0r\0")
+    TESTCASE (L"foobar", +0, TRUE, 12 - 1, FALSE, 12 - 1, "f\0o\0o\0b\0a\0r")
+    TESTCASE (L"foobar", +0, TRUE, 12 - 2, FALSE, 12 - 2, "f\0o\0o\0b\0a\0")
+    TESTCASE (L"foobar", +0, TRUE, 12 - 3, FALSE, 12 - 3, "f\0o\0o\0b\0a")
+    TESTCASE (L"foobar", -1, TRUE, 12 + 3, FALSE, 12 - 1, "f\0o\0o\0b\0a\0r")
+    TESTCASE (L"foobar", -1, TRUE, 12 + 2, FALSE, 12 - 1, "f\0o\0o\0b\0a\0r")
+    TESTCASE (L"foobar", -1, TRUE, 12 + 1, FALSE, 12 - 1, "f\0o\0o\0b\0a\0r")
+    TESTCASE (L"foobar", -1, TRUE, 12 + 0, FALSE, 12 - 1, "f\0o\0o\0b\0a\0r")
+    TESTCASE (L"foobar", -1, TRUE, 12 - 1, FALSE, 12 - 1, "f\0o\0o\0b\0a\0r")
+    TESTCASE (L"foobar", -1, TRUE, 12 - 2, FALSE, 12 - 2, "f\0o\0o\0b\0a\0")
+    TESTCASE (L"foobar", -1, TRUE, 12 - 3, FALSE, 12 - 3, "f\0o\0o\0b\0a")
+    TESTCASE (L"foobar", -1, TRUE, 12 - 4, FALSE, 12 - 4, "f\0o\0o\0b\0")
+    TESTCASE (L"foobar", -2, TRUE, 12 + 2, FALSE, 12 - 2, "f\0o\0o\0b\0a\0")
+    TESTCASE (L"foobar", -2, TRUE, 12 + 1, FALSE, 12 - 2, "f\0o\0o\0b\0a\0")
+    TESTCASE (L"foobar", -2, TRUE, 12 + 0, FALSE, 12 - 2, "f\0o\0o\0b\0a\0")
+    TESTCASE (L"foobar", -2, TRUE, 12 - 1, FALSE, 12 - 2, "f\0o\0o\0b\0a\0")
+    TESTCASE (L"foobar", -2, TRUE, 12 - 2, FALSE, 12 - 2, "f\0o\0o\0b\0a\0")
+    TESTCASE (L"foobar", -2, TRUE, 12 - 3, FALSE, 12 - 3, "f\0o\0o\0b\0a")
+    TESTCASE (L"foobar", -2, TRUE, 12 - 4, FALSE, 12 - 4, "f\0o\0o\0b\0")
+    TESTCASE (L"foobar", -2, TRUE, 12 - 5, FALSE, 12 - 5, "f\0o\0o\0b")
+    TESTCASE (L"foobar", +2, TRUE, 12 + 4, TRUE, 12 + 2, "f\0o\0o\0b\0a\0r\0\0\0")
+    TESTCASE (L"foobar", +2, TRUE, 12 + 3, TRUE, 12 + 2, "f\0o\0o\0b\0a\0r\0\0\0")
+    TESTCASE (L"foobar", +2, TRUE, 12 + 2, TRUE, 12 + 2, "f\0o\0o\0b\0a\0r\0\0\0")
+    TESTCASE (L"foobar", +2, TRUE, 12 + 1, TRUE, 12 + 1, "f\0o\0o\0b\0a\0r\0\0")
+    TESTCASE (L"foobar", +2, TRUE, 12 + 0, TRUE, 12 + 0, "f\0o\0o\0b\0a\0\0\0")
+    TESTCASE (L"foobar", +2, TRUE, 12 - 1, TRUE, 12 - 1, "f\0o\0o\0b\0a\0\0")
+    TESTCASE (L"foobar", +2, TRUE, 12 - 2, TRUE, 12 - 2, "f\0o\0o\0b\0\0\0")
+    TESTCASE (L"foobar", +2, TRUE, 12 - 3, TRUE, 12 - 3, "f\0o\0o\0b\0\0")
+    TESTCASE (L"foobar", +1, TRUE, 12 + 4, TRUE, 12 + 1, "f\0o\0o\0b\0a\0r\0\0")
+    TESTCASE (L"foobar", +1, TRUE, 12 + 3, TRUE, 12 + 1, "f\0o\0o\0b\0a\0r\0\0")
+    TESTCASE (L"foobar", +1, TRUE, 12 + 2, TRUE, 12 + 1, "f\0o\0o\0b\0a\0r\0\0")
+    TESTCASE (L"foobar", +1, TRUE, 12 + 1, TRUE, 12 + 1, "f\0o\0o\0b\0a\0r\0\0")
+    TESTCASE (L"foobar", +1, TRUE, 12 + 0, TRUE, 12 + 0, "f\0o\0o\0b\0a\0\0\0")
+    TESTCASE (L"foobar", +1, TRUE, 12 - 1, TRUE, 12 - 1, "f\0o\0o\0b\0a\0\0")
+    TESTCASE (L"foobar", +1, TRUE, 12 - 2, TRUE, 12 - 2, "f\0o\0o\0b\0\0\0")
+    TESTCASE (L"foobar", +1, TRUE, 12 - 3, TRUE, 12 - 3, "f\0o\0o\0b\0\0")
+    TESTCASE (L"foobar", +0, TRUE, 12 + 4, TRUE, 12 + 1, "f\0o\0o\0b\0a\0r\0\0")
+    TESTCASE (L"foobar", +0, TRUE, 12 + 3, TRUE, 12 + 1, "f\0o\0o\0b\0a\0r\0\0")
+    TESTCASE (L"foobar", +0, TRUE, 12 + 2, TRUE, 12 + 1, "f\0o\0o\0b\0a\0r\0\0")
+    TESTCASE (L"foobar", +0, TRUE, 12 + 1, TRUE, 12 + 1, "f\0o\0o\0b\0a\0r\0\0")
+    TESTCASE (L"foobar", +0, TRUE, 12 + 0, TRUE, 12 + 0, "f\0o\0o\0b\0a\0\0\0")
+    TESTCASE (L"foobar", +0, TRUE, 12 - 1, TRUE, 12 - 1, "f\0o\0o\0b\0a\0\0")
+    TESTCASE (L"foobar", +0, TRUE, 12 - 2, TRUE, 12 - 2, "f\0o\0o\0b\0\0\0")
+    TESTCASE (L"foobar", +0, TRUE, 12 - 3, TRUE, 12 - 3, "f\0o\0o\0b\0\0")
+    TESTCASE (L"foobar", -1, TRUE, 12 + 3, TRUE, 12 + 1, "f\0o\0o\0b\0a\0r\0\0")
+    TESTCASE (L"foobar", -1, TRUE, 12 + 2, TRUE, 12 + 1, "f\0o\0o\0b\0a\0r\0\0")
+    TESTCASE (L"foobar", -1, TRUE, 12 + 1, TRUE, 12 + 1, "f\0o\0o\0b\0a\0r\0\0")
+    TESTCASE (L"foobar", -1, TRUE, 12 + 0, TRUE, 12 + 0, "f\0o\0o\0b\0a\0\0\0")
+    TESTCASE (L"foobar", -1, TRUE, 12 - 1, TRUE, 12 - 1, "f\0o\0o\0b\0a\0\0")
+    TESTCASE (L"foobar", -1, TRUE, 12 - 2, TRUE, 12 - 2, "f\0o\0o\0b\0\0\0")
+    TESTCASE (L"foobar", -1, TRUE, 12 - 3, TRUE, 12 - 3, "f\0o\0o\0b\0\0")
+    TESTCASE (L"foobar", -1, TRUE, 12 - 4, TRUE, 12 - 4, "f\0o\0o\0\0\0")
+    TESTCASE (L"foobar", -2, TRUE, 12 + 2, TRUE, 12 - 1, "f\0o\0o\0b\0a\0\0")
+    TESTCASE (L"foobar", -2, TRUE, 12 + 1, TRUE, 12 - 1, "f\0o\0o\0b\0a\0\0")
+    TESTCASE (L"foobar", -2, TRUE, 12 + 0, TRUE, 12 - 1, "f\0o\0o\0b\0a\0\0")
+    TESTCASE (L"foobar", -2, TRUE, 12 - 1, TRUE, 12 - 1, "f\0o\0o\0b\0a\0\0")
+    TESTCASE (L"foobar", -2, TRUE, 12 - 2, TRUE, 12 - 2, "f\0o\0o\0b\0\0\0")
+    TESTCASE (L"foobar", -2, TRUE, 12 - 3, TRUE, 12 - 3, "f\0o\0o\0b\0\0")
+    TESTCASE (L"foobar", -2, TRUE, 12 - 4, TRUE, 12 - 4, "f\0o\0o\0\0\0")
+    TESTCASE (L"foobar", -2, TRUE, 12 - 5, TRUE, 12 - 5, "f\0o\0o\0\0")
+    TESTCASE (L"foobar", +2, FALSE, 0, FALSE, 12 + 2, "f\0o\0o\0b\0a\0r\0\0\0")
+    TESTCASE (L"foobar", +1, FALSE, 0, FALSE, 12 + 1, "f\0o\0o\0b\0a\0r\0\0")
+    TESTCASE (L"foobar", +0, FALSE, 0, FALSE, 12 + 0, "f\0o\0o\0b\0a\0r\0")
+    TESTCASE (L"foobar", -1, FALSE, 0, FALSE, 12 - 1, "f\0o\0o\0b\0a\0r")
+    TESTCASE (L"foobar", -2, FALSE, 0, FALSE, 12 - 2, "f\0o\0o\0b\0a\0")
+    TESTCASE (L"foobar", +2, FALSE, 0, TRUE, 12 + 2, "f\0o\0o\0b\0a\0r\0\0\0")
+    TESTCASE (L"foobar", +1, FALSE, 0, TRUE, 12 + 1, "f\0o\0o\0b\0a\0r\0\0")
+    TESTCASE (L"foobar", +0, FALSE, 0, TRUE, 12 + 1, "f\0o\0o\0b\0a\0r\0\0")
+    TESTCASE (L"foobar", -1, FALSE, 0, TRUE, 12 + 1, "f\0o\0o\0b\0a\0r\0\0")
+    TESTCASE (L"foobar", -2, FALSE, 0, TRUE, 12 - 1, "f\0o\0o\0b\0a\0\0")
+    TESTCASE (L"x", +2, TRUE, 2 + 4, FALSE, 2 + 2, "x\0\0\0")
+    TESTCASE (L"x", +2, TRUE, 2 + 3, FALSE, 2 + 2, "x\0\0\0")
+    TESTCASE (L"x", +2, TRUE, 2 + 2, FALSE, 2 + 2, "x\0\0\0")
+    TESTCASE (L"x", +2, TRUE, 2 + 1, FALSE, 2 + 1, "x\0\0")
+    TESTCASE (L"x", +2, TRUE, 2 + 0, FALSE, 2 + 0, "x\0")
+    TESTCASE (L"x", +2, TRUE, 2 - 1, FALSE, 2 - 1, "x")
+    TESTCASE (L"x", +2, TRUE, 2 - 2, FALSE, 2 - 2, "")
+    TESTCASE (L"x", +1, TRUE, 2 + 3, FALSE, 2 + 1, "x\0\0")
+    TESTCASE (L"x", +1, TRUE, 2 + 2, FALSE, 2 + 1, "x\0\0")
+    TESTCASE (L"x", +1, TRUE, 2 + 1, FALSE, 2 + 1, "x\0\0")
+    TESTCASE (L"x", +1, TRUE, 2 + 0, FALSE, 2 + 0, "x\0")
+    TESTCASE (L"x", +1, TRUE, 2 - 1, FALSE, 2 - 1, "x")
+    TESTCASE (L"x", +1, TRUE, 2 - 2, FALSE, 2 - 2, "")
+    TESTCASE (L"x", +0, TRUE, 2 + 2, FALSE, 2 + 0, "x\0")
+    TESTCASE (L"x", +0, TRUE, 2 + 1, FALSE, 2 + 0, "x\0")
+    TESTCASE (L"x", +0, TRUE, 2 + 0, FALSE, 2 + 0, "x\0")
+    TESTCASE (L"x", +0, TRUE, 2 - 1, FALSE, 2 - 1, "x")
+    TESTCASE (L"x", +0, TRUE, 2 - 2, FALSE, 2 - 2, "")
+    TESTCASE (L"x", -1, TRUE, 2 + 1, FALSE, 2 - 1, "x")
+    TESTCASE (L"x", -1, TRUE, 2 + 0, FALSE, 2 - 1, "x")
+    TESTCASE (L"x", -1, TRUE, 2 - 1, FALSE, 2 - 1, "x")
+    TESTCASE (L"x", -1, TRUE, 2 - 2, FALSE, 2 - 2, "")
+    TESTCASE (L"x", -2, TRUE, 2 + 0, FALSE, 2 - 2, "")
+    TESTCASE (L"x", -2, TRUE, 2 - 1, FALSE, 2 - 2, "")
+    TESTCASE (L"x", -2, TRUE, 2 - 2, FALSE, 2 - 2, "")
+    TESTCASE (L"x", +2, TRUE, 2 + 4, TRUE, 2 + 2, "x\0\0\0")
+    TESTCASE (L"x", +2, TRUE, 2 + 3, TRUE, 2 + 2, "x\0\0\0")
+    TESTCASE (L"x", +2, TRUE, 2 + 2, TRUE, 2 + 2, "x\0\0\0")
+    TESTCASE (L"x", +2, TRUE, 2 + 1, TRUE, 2 + 1, "x\0\0")
+    TESTCASE (L"x", +2, TRUE, 2 + 0, TRUE, 2 + 0, "\0\0")
+    TESTCASE (L"x", +2, TRUE, 2 - 1, TRUE, 2 - 1, "\0")
+    TESTCASE (L"x", +2, TRUE, 2 - 2, TRUE, 2 - 2, "")
+    TESTCASE (L"x", +1, TRUE, 2 + 3, TRUE, 2 + 1, "x\0\0")
+    TESTCASE (L"x", +1, TRUE, 2 + 2, TRUE, 2 + 1, "x\0\0")
+    TESTCASE (L"x", +1, TRUE, 2 + 1, TRUE, 2 + 1, "x\0\0")
+    TESTCASE (L"x", +1, TRUE, 2 + 0, TRUE, 2 + 0, "\0\0")
+    TESTCASE (L"x", +1, TRUE, 2 - 1, TRUE, 2 - 1, "\0")
+    TESTCASE (L"x", +1, TRUE, 2 - 2, TRUE, 2 - 2, "")
+    TESTCASE (L"x", +0, TRUE, 2 + 2, TRUE, 2 + 1, "x\0\0")
+    TESTCASE (L"x", +0, TRUE, 2 + 1, TRUE, 2 + 1, "x\0\0")
+    TESTCASE (L"x", +0, TRUE, 2 + 0, TRUE, 2 + 0, "\0\0")
+    TESTCASE (L"x", +0, TRUE, 2 - 1, TRUE, 2 - 1, "\0")
+    TESTCASE (L"x", +0, TRUE, 2 - 2, TRUE, 2 - 2, "")
+    TESTCASE (L"x", -1, TRUE, 2 + 1, TRUE, 2 + 1, "x\0\0")
+    TESTCASE (L"x", -1, TRUE, 2 + 0, TRUE, 2 + 0, "\0\0")
+    TESTCASE (L"x", -1, TRUE, 2 - 1, TRUE, 2 - 1, "\0")
+    TESTCASE (L"x", -1, TRUE, 2 - 2, TRUE, 2 - 2, "")
+    TESTCASE (L"x", -2, TRUE, 2 + 0, TRUE, 2 - 2, "")
+    TESTCASE (L"x", -2, TRUE, 2 - 1, TRUE, 2 - 2, "")
+    TESTCASE (L"x", -2, TRUE, 2 - 2, TRUE, 2 - 2, "")
+    TESTCASE (L"x", +2, FALSE, 0, FALSE, 2 + 2, "x\0\0\0")
+    TESTCASE (L"x", +1, FALSE, 0, FALSE, 2 + 1, "x\0\0")
+    TESTCASE (L"x", +0, FALSE, 0, FALSE, 2 + 0, "x\0")
+    TESTCASE (L"x", -1, FALSE, 0, FALSE, 2 - 1, "x")
+    TESTCASE (L"x", -2, FALSE, 0, FALSE, 2 - 2, "")
+    TESTCASE (L"x", +2, FALSE, 0, TRUE, 2 + 2, "x\0\0\0")
+    TESTCASE (L"x", +1, FALSE, 0, TRUE, 2 + 1, "x\0\0")
+    TESTCASE (L"x", +0, FALSE, 0, TRUE, 2 + 1, "x\0\0")
+    TESTCASE (L"x", -1, FALSE, 0, TRUE, 2 + 1, "x\0\0")
+    TESTCASE (L"x", -2, FALSE, 0, TRUE, 2 - 2, "")
+    { 0, },
+  };
+#undef TESTCASE
+
+  for (i = 0; testcases[i].data != NULL; i++)
+    {
+      gunichar2 *buf;
+      int result;
+      gchar *buf_hex, *expected_hex;
+      if (testcases[i].use_buf)
+        buf = g_malloc0 (testcases[i].buf_size + 1); /* +1 to ensure it succeeds with buf_size == 0 */
+      else
+        buf = NULL;
+      result = _g_win32_copy_and_maybe_terminate (testcases[i].data,
+                                                  testcases[i].data_size,
+                                                  testcases[i].use_buf ? buf : NULL,
+                                                  testcases[i].buf_size,
+                                                  testcases[i].use_buf ? NULL : &buf,
+                                                  testcases[i].terminate);
+      if (testcases[i].reported_len != result)
+        g_error ("Test %" G_GSIZE_FORMAT " failed, result %d != %d", i, result, testcases[i].reported_len);
+      if (buf == NULL && testcases[i].buf_size != 0)
+        g_error ("Test %" G_GSIZE_FORMAT " failed, buf == NULL", i);
+      g_assert_cmpint (testcases[i].reported_len, ==, result);
+      if ((testcases[i].use_buf && testcases[i].buf_size != 0) ||
+          (!testcases[i].use_buf && testcases[i].reported_len != 0))
+        {
+          g_assert_nonnull (buf);
+          buf_hex = to_hex ((const guchar *) buf, result);
+          expected_hex = to_hex (testcases[i].returned_string, testcases[i].reported_len);
+          if (memcmp (buf, testcases[i].returned_string, result) != 0)
+            g_error ("Test %" G_GSIZE_FORMAT " failed:\n%s !=\n%s", i, buf_hex, expected_hex);
+          g_assert_memcmp (buf, ==, testcases[i].returned_string, testcases[i].reported_len, buf_hex, 
expected_hex, testcases[i].line);
+          g_free (buf_hex);
+          g_free (expected_hex);
+        }
+      g_free (buf);
+    }
+}
+
 #endif
 
 int
@@ -1165,6 +1392,7 @@ main (int   argc,
 
 #ifdef G_OS_WIN32
   g_test_add_func ("/fileutils/stdio-win32-pathstrip", test_win32_pathstrip);
+  g_test_add_func ("/fileutils/stdio-win32-zero-terminate-symlink", test_win32_zero_terminate_symlink);
 #endif
   g_test_add_func ("/fileutils/build-path", test_build_path);
   g_test_add_func ("/fileutils/build-pathv", test_build_pathv);


[Date Prev][Date Next]   [Thread Prev][Thread Next]   [Thread Index] [Date Index] [Author Index]