/* vi:set et ai sw=2 sts=2 ts=2: */
/*-
 * Copyright (c) 2009-2010 Jannis Pohlmann <jannis@xfce.org>
 *
 * This program is free software; you can redistribute it and/or
 * modify it under the terms of the GNU General Public License as
 * published by the Free Software Foundation; either version 2 of
 * the License, or (at your option) any later version.
 *
 * This program is distributed in the hope that it will be useful,
 * but WITHOUT ANY WARRANTY; without even the implied warranty of
 * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
 * GNU General Public License for more details.
 *
 * You should have received a copy of the GNU General Public
 * License along with this program; if not, write to the Free
 * Software Foundation, Inc., 51 Franklin Street, Fifth Floor,
 * Boston, MA 02110-1301, USA.
 */

#ifdef __linux__
#define _GNU_SOURCE
#endif

#ifdef HAVE_STDLIB_H
#include <stdlib.h>
#endif

#ifdef HAVE_STRING_H
#include <string.h>
#endif

#include <gio/gio.h>

#ifdef HAVE_GIO_UNIX

#ifdef __linux__

#define HAVE_DIRECT_IO 1
#include <fcntl.h>
#include <gio/gunixinputstream.h>

#ifdef HAVE_STATX
#include <sys/stat.h>
#ifdef STATX_DIOALIGN
#define HAVE_STATX_DIOALIGN 1
#endif /* STATX_DIOALIGN */
#endif /* HAVE_STATX */

#endif /* __linux__ */

#include <gio/gdesktopappinfo.h>
#include <gio/gunixmounts.h>

#endif /* HAVE_GIO_UNIX */

#include "thunar/thunar-file.h"
#include "thunar/thunar-gio-extensions.h"
#include "thunar/thunar-preferences.h"
#include "thunar/thunar-private.h"
#include "thunar/thunar-util.h"

#include <libxfce4util/libxfce4util.h>

#define CMP_BUF_MIN_ALIGN (16)
#define CMP_BUF_SIZE (1024 * 512)

#ifndef O_BINARY
#define O_BINARY (0)
#endif



/* See : https://freedesktop.org/wiki/Specifications/icon-naming-spec/ */
static struct
{
  const gchar *icon_name;
  const gchar *type;
}

/* clang-format off */
device_icon_name [] =
{
  /* Implementation specific */
  { "multimedia-player-apple-ipod-touch" , N_("iPod touch") },
  { "computer-apple-ipad"                , N_("iPad") },
  { "phone-apple-iphone"                 , N_("iPhone") },
  { "drive-harddisk-solidstate"          , N_("Solid State Drive") },
  { "drive-harddisk-system"              , N_("System Drive") },
  { "drive-harddisk-usb"                 , N_("USB Drive") },
  { "drive-removable-media-usb"          , N_("USB Drive") },

  /* Freedesktop icon-naming-spec */
  { "camera-photo*"          , N_("Camera") },
  { "drive-harddisk*"        , N_("Harddisk") },
  { "drive-optical*"         , N_("Optical Drive") },
  { "drive-removable-media*" , N_("Removable Drive") },
  { "media-flash*"           , N_("Flash Drive") },
  { "media-floppy*"          , N_("Floppy") },
  { "media-optical*"         , N_("Optical Media") },
  { "media-tape*"            , N_("Tape") },
  { "multimedia-player*"     , N_("Multimedia Player") },
  { "pda*"                   , N_("PDA") },
  { "phone*"                 , N_("Phone") },
  { NULL                     , NULL }
};
/* clang-format on */



static const gchar *
guess_device_type_from_icon_name (const gchar *icon_name);
static GFileInfo *
thunar_g_file_get_content_type_query_info (GFile   *gfile,
                                           GError **err);
static void
thunar_g_file_info_set_attribute (GFileInfo   *info,
                                  ThunarGType  type,
                                  const gchar *setting_name,
                                  const gchar *setting_value);

GFile *
thunar_g_file_new_for_home (void)
{
  return g_file_new_for_path (xfce_get_homedir ());
}



GFile *
thunar_g_file_new_for_root (void)
{
  return g_file_new_for_uri ("file:///");
}



GFile *
thunar_g_file_new_for_recent (void)
{
  return g_file_new_for_uri ("recent:///");
}



GFile *
thunar_g_file_new_for_trash (void)
{
  return g_file_new_for_uri ("trash:///");
}



GFile *
thunar_g_file_new_for_computer (void)
{
  return g_file_new_for_uri ("computer://");
}



GFile *
thunar_g_file_new_for_network (void)
{
  return g_file_new_for_uri ("network://");
}



GFile *
thunar_g_file_new_for_desktop (void)
{
  return g_file_new_for_path (g_get_user_special_dir (G_USER_DIRECTORY_DESKTOP));
}



GFile *
thunar_g_file_new_for_bookmarks (void)
{
  gchar *filename;
  GFile *bookmarks;

  filename = g_build_filename (g_get_user_config_dir (), "gtk-3.0", "bookmarks", NULL);
  bookmarks = g_file_new_for_path (filename);
  g_free (filename);

  return bookmarks;
}



/* internal version, for recursive execution. Uses a lookup table to detect loops */
static GFile *
_thunar_g_file_resolve_symlink_internal (GFile *file, GHashTable *lookup_table)
{
  g_autoptr (GHashTable) lookup_history = NULL;
  g_autoptr (GFileInfo) info = NULL;

  _thunar_return_val_if_fail (G_IS_FILE (file), NULL);

  if (lookup_table == NULL)
    lookup_history = g_hash_table_new_full (g_str_hash, g_str_equal, g_free, NULL);
  else
    lookup_history = g_hash_table_ref (lookup_table);

  /* Resolve the link target recursively, until the target we found is not a symlink */
  char *file_uri = g_file_get_uri (file);

  if (file_uri == NULL)
    return NULL;

  /* Detect loops: if the file already was seen, return NULL */
  if (g_hash_table_contains (lookup_history, file_uri))
    {
      g_info ("Symlink loop detected - resolve symlink aborted");
      g_free (file_uri);
      return NULL;
    }
  g_hash_table_add (lookup_history, file_uri);

  info = g_file_query_info (file,
                            G_FILE_ATTRIBUTE_STANDARD_SYMLINK_TARGET "," G_FILE_ATTRIBUTE_STANDARD_IS_SYMLINK,
                            G_FILE_QUERY_INFO_NONE, NULL, NULL);
  if (info == NULL)
    return NULL;

  /* Not a symlink: return that file */
  if (!g_file_info_get_attribute_boolean (info, G_FILE_ATTRIBUTE_STANDARD_IS_SYMLINK))
    return g_object_ref (file);

  /* It's a symlink, get the target */
  const char *target_path = g_file_info_get_attribute_byte_string (info, G_FILE_ATTRIBUTE_STANDARD_SYMLINK_TARGET);
  if (target_path == NULL)
    {
      g_info ("Failed to read target path from symlink: %s", g_file_get_path (file));
      return NULL;
    }

  /* The target is as well a symlink ... so lets use recursion */
  if (g_path_is_absolute (target_path))
    {
      g_autoptr (GFile) target = g_file_new_for_path (target_path);
      return _thunar_g_file_resolve_symlink_internal (target, lookup_history);
    }
  else
    {
      /* Resolve a possible relative path */
      g_autoptr (GFile) target = NULL;
      g_autoptr (GFile) parent = NULL;
      g_autoptr (GFile) parent_resolved = NULL;

      parent = g_file_get_parent (file);
      if (parent == NULL)
        return NULL;

      /* Resolve parent, if required */
      parent_resolved = _thunar_g_file_resolve_symlink_internal (parent, lookup_history);

      if (parent_resolved == NULL)
        {
          g_info ("Failed to resolve parent of symlink");
          return NULL;
        }

      target = g_file_resolve_relative_path (parent_resolved, target_path);

      /* Sicne as well that file might be a symlink, we need to use recursion */
      return _thunar_g_file_resolve_symlink_internal (target, lookup_history);
    }
}



/**
 * thunar_g_file_resolve_symlink:
 * @file : a #GFile
 *
 * Returns the resolved symlink target of @file as a new #GFile.
 * If @file is not a symlink, @file will just be returned
 *
 * Return value: (nullable) (transfer full): A #GFile on success and %NULL on failure.
 **/
GFile *
thunar_g_file_resolve_symlink (GFile *file)
{
  return _thunar_g_file_resolve_symlink_internal (file, NULL);
}



gboolean
thunar_g_file_is_root (GFile *file)
{
  GFile   *parent;
  gboolean is_root = TRUE;

  parent = g_file_get_parent (file);
  if (G_UNLIKELY (parent != NULL))
    {
      is_root = FALSE;
      g_object_unref (parent);
    }

  return is_root;
}



gboolean
thunar_g_file_is_trashed (GFile *file)
{
  _thunar_return_val_if_fail (G_IS_FILE (file), FALSE);
  return g_file_has_uri_scheme (file, "trash");
}



gboolean
thunar_g_file_is_in_recent (GFile *file)
{
  _thunar_return_val_if_fail (G_IS_FILE (file), FALSE);
  return g_file_has_uri_scheme (file, "recent");
}



gboolean
thunar_g_file_is_home (GFile *file)
{
  GFile   *home;
  gboolean is_home = FALSE;

  _thunar_return_val_if_fail (G_IS_FILE (file), FALSE);

  home = thunar_g_file_new_for_home ();
  is_home = g_file_equal (home, file);
  g_object_unref (home);

  return is_home;
}



gboolean
thunar_g_file_is_trash (GFile *file)
{
  char    *uri;
  gboolean is_trash = FALSE;

  _thunar_return_val_if_fail (G_IS_FILE (file), FALSE);

  uri = g_file_get_uri (file);
  is_trash = g_strcmp0 (uri, "trash:///") == 0;
  g_free (uri);

  return is_trash;
}



gboolean
thunar_g_file_is_recent (GFile *file)
{
  char    *uri;
  gboolean is_recent = FALSE;

  _thunar_return_val_if_fail (G_IS_FILE (file), FALSE);

  uri = g_file_get_uri (file);
  is_recent = g_strcmp0 (uri, "recent:///") == 0;
  g_free (uri);

  return is_recent;
}



gboolean
thunar_g_file_is_computer (GFile *file)
{
  char    *uri;
  gboolean is_computer = FALSE;

  _thunar_return_val_if_fail (G_IS_FILE (file), FALSE);

  uri = g_file_get_uri (file);
  is_computer = g_strcmp0 (uri, "computer:///") == 0;
  g_free (uri);

  return is_computer;
}



gboolean
thunar_g_file_is_network (GFile *file)
{
  char    *uri;
  gboolean is_network = FALSE;

  _thunar_return_val_if_fail (G_IS_FILE (file), FALSE);

  uri = g_file_get_uri (file);
  is_network = g_strcmp0 (uri, "network:///") == 0;
  g_free (uri);

  return is_network;
}



GKeyFile *
thunar_g_file_query_key_file (GFile        *file,
                              GCancellable *cancellable,
                              GError      **error)
{
  GKeyFile *key_file;
  gchar    *contents = NULL;
  gsize     length;

  _thunar_return_val_if_fail (G_IS_FILE (file), NULL);
  _thunar_return_val_if_fail (cancellable == NULL || G_IS_CANCELLABLE (cancellable), NULL);
  _thunar_return_val_if_fail (error == NULL || *error == NULL, NULL);

  /* try to load the entire file into memory */
  if (!g_file_load_contents (file, cancellable, &contents, &length, NULL, error))
    return NULL;

  /* allocate a new key file */
  key_file = g_key_file_new ();

  /* try to parse the key file from the contents of the file */
  if (G_LIKELY (length == 0
                || g_key_file_load_from_data (key_file, contents, length,
                                              G_KEY_FILE_KEEP_COMMENTS
                                              | G_KEY_FILE_KEEP_TRANSLATIONS,
                                              error)))
    {
      g_free (contents);
      return key_file;
    }
  else
    {
      g_free (contents);
      g_key_file_free (key_file);
      return NULL;
    }
}



gboolean
thunar_g_file_write_key_file (GFile        *file,
                              GKeyFile     *key_file,
                              GCancellable *cancellable,
                              GError      **error)
{
  gchar   *contents;
  gsize    length;
  gboolean result = TRUE;

  _thunar_return_val_if_fail (G_IS_FILE (file), FALSE);
  _thunar_return_val_if_fail (key_file != NULL, FALSE);
  _thunar_return_val_if_fail (cancellable == NULL || G_IS_CANCELLABLE (cancellable), FALSE);
  _thunar_return_val_if_fail (error == NULL || *error == NULL, FALSE);

  /* write the key file into the contents buffer */
  contents = g_key_file_to_data (key_file, &length, NULL);

  /* try to replace the file contents with the key file data */
  if (contents != NULL)
    {
      result = g_file_replace_contents (file, contents, length, NULL, FALSE,
                                        G_FILE_CREATE_NONE,
                                        NULL, cancellable, error);

      /* cleanup */
      g_free (contents);
    }

  return result;
}



gchar *
thunar_g_file_get_location (GFile *file)
{
  gchar *location;

  _thunar_return_val_if_fail (G_IS_FILE (file), NULL);

  location = g_file_get_path (file);
  if (location == NULL)
    location = g_file_get_uri (file);

  return location;
}



static const gchar *
guess_device_type_from_icon_name (const gchar *icon_name)
{
  for (int i = 0; device_icon_name[i].type != NULL; i++)
    {
      if (g_pattern_match_simple (device_icon_name[i].icon_name, icon_name))
        return g_dgettext (NULL, device_icon_name[i].type);
    }
  return NULL;
}



/**
 * thunar_g_file_guess_device_type:
 * @file : a #GFile instance.
 *
 * Returns : (transfer none) (nullable): the string of the device type.
 */
const gchar *
thunar_g_file_guess_device_type (GFile *file)
{
  GFileInfo   *fileinfo = NULL;
  GIcon       *icon = NULL;
  const gchar *icon_name = NULL;
  const gchar *device_type = NULL;

  _thunar_return_val_if_fail (G_IS_FILE (file), NULL);

  fileinfo = g_file_query_info (file,
                                G_FILE_ATTRIBUTE_STANDARD_ICON,
                                G_FILE_QUERY_INFO_NONE, NULL, NULL);
  if (fileinfo == NULL)
    return NULL;

  icon = G_ICON (g_file_info_get_attribute_object (fileinfo, G_FILE_ATTRIBUTE_STANDARD_ICON));
  if (!G_IS_THEMED_ICON (icon))
    {
      g_object_unref (fileinfo);
      return NULL;
    }

  icon_name = g_themed_icon_get_names (G_THEMED_ICON (icon))[0];
  device_type = guess_device_type_from_icon_name (icon_name);
  g_object_unref (fileinfo);

  return device_type;
}



gchar *
thunar_g_file_get_display_name (GFile *file)
{
  gchar *base_name;
  gchar *display_name;

  _thunar_return_val_if_fail (G_IS_FILE (file), NULL);

  base_name = g_file_get_basename (file);
  if (G_LIKELY (base_name != NULL))
    {
      if (strcmp (base_name, "/") == 0)
        display_name = g_strdup (_("File System"));
      else if (thunar_g_file_is_trash (file))
        display_name = g_strdup (_("Trash"));
      else if (g_utf8_validate (base_name, -1, NULL))
        display_name = g_strdup (base_name);
      else
        display_name = g_uri_escape_string (base_name, G_URI_RESERVED_CHARS_ALLOWED_IN_PATH, TRUE);

      g_free (base_name);
    }
  else
    {
      display_name = g_strdup ("?");
    }

  return display_name;
}



gchar *
thunar_g_file_get_display_name_remote (GFile *mount_point)
{
  gchar       *scheme;
  gchar       *parse_name;
  const gchar *p;
  const gchar *path;
  gchar       *unescaped;
  gchar       *hostname;
  gchar       *display_name = NULL;
  const gchar *skip;
  const gchar *firstdot;
  const gchar  skip_chars[] = ":@";
  guint        n;

  _thunar_return_val_if_fail (G_IS_FILE (mount_point), NULL);

  /* not intended for local mounts */
  if (!g_file_is_native (mount_point))
    {
      scheme = g_file_get_uri_scheme (mount_point);
      parse_name = g_file_get_parse_name (mount_point);

      if (scheme != NULL && g_str_has_prefix (parse_name, scheme))
        {
          /* extract the hostname */
          p = parse_name + strlen (scheme);
          while (*p == ':' || *p == '/')
            ++p;

          /* goto path part */
          path = strchr (p, '/');
          firstdot = strchr (p, '.');

          if (firstdot != NULL)
            {
              /* skip password or login names in the hostname */
              for (n = 0; n < G_N_ELEMENTS (skip_chars) - 1; n++)
                {
                  skip = strchr (p, skip_chars[n]);
                  if (skip != NULL
                      && (path == NULL || skip < path)
                      && (skip < firstdot))
                    p = skip + 1;
                }
            }

          /* extract the path and hostname from the string */
          if (G_LIKELY (path != NULL))
            {
              hostname = g_strndup (p, path - p);
            }
          else
            {
              hostname = g_strdup (p);
              path = "/";
            }

          /* unescape the path so that spaces and other characters are shown correctly */
          unescaped = g_uri_unescape_string (path, NULL);

          /* TRANSLATORS: this will result in "<path> on <hostname>" */
          display_name = g_strdup_printf (_("%s on %s"), unescaped, hostname);

          g_free (unescaped);
          g_free (hostname);
        }

      if (scheme != NULL)
        g_free (scheme);
      g_free (parse_name);
    }

  /* never return null */
  if (display_name == NULL)
    display_name = thunar_g_file_get_display_name (mount_point);

  return display_name;
}



gboolean
thunar_g_vfs_is_uri_scheme_supported (const gchar *scheme)
{
  const gchar *const *supported_schemes;
  gboolean            supported = FALSE;
  guint               n;
  GVfs               *gvfs;

  _thunar_return_val_if_fail (scheme != NULL && *scheme != '\0', FALSE);

  gvfs = g_vfs_get_default ();
  supported_schemes = g_vfs_get_supported_uri_schemes (gvfs);

  if (supported_schemes == NULL)
    return FALSE;

  for (n = 0; !supported && supported_schemes[n] != NULL; ++n)
    if (g_strcmp0 (supported_schemes[n], scheme) == 0)
      supported = TRUE;

  return supported;
}



/**
 * thunar_g_file_get_fs_type:
 * @file : a #GFile instance.
 *
 * Returns : (transfer full) (nullable): the filesystem type of @file or %NULL if not found.
 **/
gchar *
thunar_g_file_get_fs_type (GFile *file)
{
  g_autoptr (GFileInfo) filesystem_info = NULL;

  _thunar_return_val_if_fail (G_IS_FILE (file), NULL);

  filesystem_info = g_file_query_filesystem_info (file,
                                                  G_FILE_ATTRIBUTE_FILESYSTEM_TYPE "," G_FILE_ATTRIBUTE_UNIX_DEVICE ",id::filesystem",
                                                  NULL, NULL);

  if (!filesystem_info)
    return NULL;

  if (!g_file_info_has_attribute (filesystem_info, G_FILE_ATTRIBUTE_FILESYSTEM_TYPE))
    return NULL;

  return g_strdup (g_file_info_get_attribute_string (filesystem_info, G_FILE_ATTRIBUTE_FILESYSTEM_TYPE));
}



/**
 * thunar_g_file_get_fs_max_name_length:
 * @file : (nullable): a #GFile instance.
 *
 * Returns the maximum length a file is allowed to have on the filesystem of @file
 * Default is 255
 *
 * Returns : The maximum length
 **/
gint
thunar_g_file_get_fs_max_name_length (GFile *file)
{
  g_autofree gchar *fs_type = NULL;

  if (file == NULL)
    return 255;

  fs_type = thunar_g_file_get_fs_type (file);
  if (g_strstr_len (fs_type, -1, "bcachefs") != NULL)
    return 512;

  return 255;
}



/**
 * thunar_g_file_get_fs_space:
 * @file                 : a #GFile instance.
 * @fs_space_info_return : Container values are filled as much as possible
 *
 * Determines the amount of free/used space and the total size of the volume on
 * which @file resides. It fills the container values of the passed struct accordingly.
 *
 **/
void
thunar_g_file_get_fs_space (GFile                     *file,
                            ThunarFilesystemSpaceInfo *fs_space_info_return)
{
  g_autoptr (GFileInfo) filesystem_info = NULL;

  _thunar_return_if_fail (G_IS_FILE (file));
  _thunar_return_if_fail (fs_space_info_return != NULL);

  filesystem_info = g_file_query_filesystem_info (file,
                                                  THUNARX_FILESYSTEM_INFO_NAMESPACE,
                                                  NULL, NULL);

  fs_space_info_return->fs_size_total_read_ok = FALSE;
  fs_space_info_return->fs_size_free_read_ok = FALSE;
  fs_space_info_return->fs_size_used_read_ok = FALSE;
  fs_space_info_return->fs_size_usable_read_ok = FALSE;

  if (filesystem_info == NULL)
    return;

  if (g_file_info_has_attribute (filesystem_info, G_FILE_ATTRIBUTE_FILESYSTEM_SIZE))
    {
      fs_space_info_return->fs_size_total = g_file_info_get_attribute_uint64 (filesystem_info, G_FILE_ATTRIBUTE_FILESYSTEM_SIZE);
      fs_space_info_return->fs_size_total_read_ok = TRUE;
    }

  if (g_file_info_has_attribute (filesystem_info, G_FILE_ATTRIBUTE_FILESYSTEM_FREE))
    {
      fs_space_info_return->fs_free_space = g_file_info_get_attribute_uint64 (filesystem_info, G_FILE_ATTRIBUTE_FILESYSTEM_FREE);
      fs_space_info_return->fs_size_free_read_ok = TRUE;
    }

  if (g_file_info_has_attribute (filesystem_info, G_FILE_ATTRIBUTE_FILESYSTEM_USED))
    {
      fs_space_info_return->fs_used_space = g_file_info_get_attribute_uint64 (filesystem_info, G_FILE_ATTRIBUTE_FILESYSTEM_USED);
      fs_space_info_return->fs_size_used_read_ok = TRUE;
    }
  else
    {
      /* Fallback. Some filesystems do not provide 'used' info. In that case, we tr to calculate the used space */
      if (fs_space_info_return->fs_size_free_read_ok == TRUE && fs_space_info_return->fs_size_total_read_ok == TRUE)
        {
          if (fs_space_info_return->fs_size_total >= fs_space_info_return->fs_free_space)
            {
              fs_space_info_return->fs_used_space = fs_space_info_return->fs_size_total - fs_space_info_return->fs_free_space;
              fs_space_info_return->fs_size_used_read_ok = TRUE;
            }
        }
    }

  /* Usable space is not provided by filesystems, it needs to be calculated */
  if (fs_space_info_return->fs_size_free_read_ok == TRUE)
    {
      if (fs_space_info_return->fs_size_used_read_ok == TRUE)
        {
          /* Note: size_total = size_free + size_used + size_reserved */
          fs_space_info_return->fs_usable_space = fs_space_info_return->fs_free_space + fs_space_info_return->fs_used_space;
          fs_space_info_return->fs_size_usable_read_ok = TRUE;
        }
      else if (fs_space_info_return->fs_size_total_read_ok == TRUE)
        {
          /* Fallback. Some filesystems do not provide 'used' info. In that case, we just assume the whole fs space can be used */
          fs_space_info_return->fs_usable_space = fs_space_info_return->fs_size_total;
          fs_space_info_return->fs_size_usable_read_ok = TRUE;
        }
    }
}



gchar *
thunar_g_file_get_free_space_string (const ThunarFilesystemSpaceInfo *fs_space_info,
                                     gboolean                         file_size_binary)
{
  g_autofree gchar *fs_size_free_str = NULL;
  g_autofree gchar *fs_size_used_str = NULL;
  gchar  *free_space_string = NULL;

  _thunar_return_val_if_fail (fs_space_info != NULL, NULL);

  if (fs_space_info->fs_size_usable_read_ok == FALSE || fs_space_info->fs_usable_space == 0)
    return NULL;

  if (fs_space_info->fs_size_free_read_ok == FALSE || fs_space_info->fs_size_used_read_ok == FALSE)
    return NULL;

  fs_size_free_str = g_format_size_full (fs_space_info->fs_free_space, file_size_binary ? G_FORMAT_SIZE_IEC_UNITS : G_FORMAT_SIZE_DEFAULT);
  fs_size_used_str = g_format_size_full (fs_space_info->fs_used_space, file_size_binary ? G_FORMAT_SIZE_IEC_UNITS : G_FORMAT_SIZE_DEFAULT);

  free_space_string = g_strdup_printf (_("%s used (%.0f%%)  |  %s free (%.0f%%)"),
                                       fs_size_used_str, (fs_space_info->fs_used_space * 100.0 / fs_space_info->fs_usable_space),
                                       fs_size_free_str, (fs_space_info->fs_free_space * 100.0 / fs_space_info->fs_usable_space));

  return free_space_string;
}



GType
thunar_g_file_list_get_type (void)
{
  static GType type = G_TYPE_INVALID;

  if (G_UNLIKELY (type == G_TYPE_INVALID))
    {
      type = g_boxed_type_register_static (I_ ("ThunarGFileList"),
                                           (GBoxedCopyFunc) thunar_g_list_copy_deep,
                                           (GBoxedFreeFunc) thunar_g_list_free_full);
    }

  return type;
}



static GHashTable *
thunar_g_hastable_copy_deep (GHashTable *table)
{
  GHashTable    *copy = g_hash_table_new_full (g_direct_hash, NULL, g_object_unref, NULL);
  GHashTableIter iter;
  gpointer       key;

  g_hash_table_iter_init (&iter, table);
  while (g_hash_table_iter_next (&iter, &key, NULL))
    g_hash_table_add (copy, g_object_ref (key));
  return copy;
}



GType
thunar_g_file_hastable_get_type (void)
{
  static GType type = G_TYPE_INVALID;

  if (G_UNLIKELY (type == G_TYPE_INVALID))
    {
      type = g_boxed_type_register_static (I_ ("ThunarGHashTableList"),
                                           (GBoxedCopyFunc) thunar_g_hastable_copy_deep,
                                           (GBoxedFreeFunc) g_hash_table_destroy);
    }

  return type;
}



/**
 * thunar_g_file_copy:
 * @source                 : input #GFile
 * @destination            : destination #GFile
 * @flags                  : set of #GFileCopyFlags
 * @use_partial            : option to use *.partial~
 * @cancellable            : (nullable): optional #GCancellable object
 * @progress_callback      : (nullable) (scope call): function to callback with progress information
 * @progress_callback_data : (clousure): user data to pass to @progress_callback
 * @error                  : (nullable): #GError to set on error
 *
 * Calls g_file_copy() if @use_partial is not enabled.
 * If enabled, copies files to *.partial~ first and then
 * renames *.partial~ into its original name.
 *
 * Return value: %TRUE on success, %FALSE otherwise.
 **/
gboolean
thunar_g_file_copy (GFile                *source,
                    GFile                *destination,
                    GFileCopyFlags        flags,
                    gboolean              use_partial,
                    GCancellable         *cancellable,
                    GFileProgressCallback progress_callback,
                    gpointer              progress_callback_data,
                    GError              **error)
{
  gboolean            success;
  GFileQueryInfoFlags query_flags;
  GFileInfo          *info = NULL;
  GFile              *parent;
  GFile              *partial;
  gchar              *partial_name;
  gchar              *base_name;

  _thunar_return_val_if_fail (g_file_has_parent (destination, NULL), FALSE);

  if (use_partial)
    {
      query_flags = (flags & G_FILE_COPY_NOFOLLOW_SYMLINKS) ? G_FILE_QUERY_INFO_NOFOLLOW_SYMLINKS : G_FILE_QUERY_INFO_NONE;
      info = g_file_query_info (source,
                                G_FILE_ATTRIBUTE_STANDARD_TYPE,
                                query_flags,
                                cancellable,
                                NULL);
    }

  /* directory does not need .partial */
  if (info == NULL)
    {
      use_partial = FALSE;
    }
  else
    {
      use_partial = g_file_info_get_file_type (info) == G_FILE_TYPE_REGULAR;
      g_clear_object (&info);
    }

  if (!use_partial)
    {
      success = g_file_copy (source, destination, flags, cancellable, progress_callback, progress_callback_data, error);
      return success;
    }

  /* check destination */
  if (g_file_query_exists (destination, NULL))
    {
      if (flags & G_FILE_COPY_OVERWRITE)
        {
          /* We want to overwrite. Just delete the old file */
          if (error != NULL)
            g_clear_error (error);
          error = NULL;
          g_file_delete (destination, NULL, error);
          if (error != NULL)
            return FALSE;
        }
      else
        {
          /* Try to mimic g_file_copy() error */
          if (error != NULL)
            *error = g_error_new (G_IO_ERROR, G_IO_ERROR_EXISTS,
                                  "Error opening file \"%s\": File exists", g_file_peek_path (destination));
          return FALSE;
        }
    }

  /* generate partial file name */
  base_name = g_file_get_basename (destination);
  if (base_name == NULL)
    {
      base_name = g_strdup ("UNNAMED");
    }

  /* limit filename length */
  partial_name = g_strdup_printf ("%.100s.partial~", base_name);
  parent = g_file_get_parent (destination);

  /* parent can't be NULL since destination must be a file */
  partial = g_file_get_child (parent, partial_name);
  g_clear_object (&parent);
  g_free (partial_name);

  /* check if partial file exists */
  if (g_file_query_exists (partial, NULL))
    g_file_delete (partial, NULL, error);

  /* copy file to .partial */
  success = g_file_copy (source, partial, flags, cancellable, progress_callback, progress_callback_data, error);

  if (success)
    {
      /* rename .partial if done without problem */
      GFile *renamed_file = g_file_set_display_name (partial, base_name, NULL, error);
      success = (renamed_file != NULL);
      if (success)
        g_object_unref (renamed_file);
    }

  if (!success)
    {
      /* try to remove incomplete file. */
      /* failure is expected so error is ignored */
      /* it must be triggered if cancelled */
      /* thus cancellable is also ignored */
      g_file_delete (partial, NULL, NULL);
    }

  g_clear_object (&partial);
  g_free (base_name);
  return success;
}



#ifdef HAVE_DIRECT_IO
static GInputStream *
open_file_and_buffer_for_direct_io (GFile        *file,
                                    GCancellable *cancellable,
                                    void        **buf_out,
                                    unsigned int *buf_align_out,
                                    size_t       *buf_size_out)
{
  int    fd = -1;
  size_t buf_align = 0;
  size_t offset_align = 0;
  size_t buf_size;
  void  *buf = NULL;
#ifdef HAVE_STATX_DIOALIGN
  struct statx statx_buf;

  // First we use statx () to ask the filesystem the required buffer alignment
  // for direct I/O (stx_dio_mem_align), as well as what the file offset
  // alignment (stx_dio_offset_align), which tells us what the buffer size is
  // required to be a multiple of.
  DBG ("Trying statx ()");
  if (statx (-1, g_file_peek_path (file), AT_STATX_SYNC_AS_STAT, STATX_DIOALIGN, &statx_buf) == 0
      && statx_buf.stx_dio_mem_align > 0
      && statx_buf.stx_dio_offset_align > 0)
    {
      DBG ("statx () succeeded, direct I/O align=%u, offset_align=%u",
           statx_buf.stx_dio_mem_align,
           statx_buf.stx_dio_offset_align);
      // Ensure alignment is at least 16, which is required for most SIMD
      // operations.  If somehow the required filesystem alignment/offset
      // alignment is lower than 16, it's always safe to go higher.
      buf_align = MAX (CMP_BUF_MIN_ALIGN, statx_buf.stx_dio_mem_align);
      offset_align = MAX (CMP_BUF_MIN_ALIGN, statx_buf.stx_dio_offset_align);
    }
#endif

  if (buf_align == 0 || offset_align == 0)
    {
      DBG ("statx () not supported or failed; using alignment/offset alignment fallback of 512");
      // Generally this fallback value should work on linux 2.6 & above.
      buf_align = 512;
      offset_align = 512;
    }

  // Since we're using blocking stdio operations and not cancellable GIO
  // operations, we should check the cancellable periodically.
  if (cancellable != NULL && g_cancellable_is_cancelled (cancellable))
    {
      goto out_err;
    }

  // Ensure the buffer size is a multiple of offset_align.
  buf_size = (CMP_BUF_SIZE / offset_align) * offset_align;
  DBG ("buf_size will be %" G_GSIZE_FORMAT " based on offset_align", buf_size);
  if (buf_size == 0)
    {
      // This shouldn't happen, but if somehow we get a truly bizarre
      // offst_align value that is larger than CMP_BUF_SIZE, we'll just bail.
      goto out_err;
    }

  // Open using the same flags g_file_read () uses, plus O_DIRECT.
  fd = open (g_file_peek_path (file), O_RDONLY | O_BINARY | O_CLOEXEC | O_DIRECT, 0);
  if (fd == -1 || (cancellable != NULL && g_cancellable_is_cancelled (cancellable)))
    {
      goto out_err;
    }
  DBG ("Successfully opened file with O_DIRECT");

  // Create a buffer to read data into, with the required alignment and size.
  if (posix_memalign (&buf, buf_align, buf_size) != 0)
    {
      goto out_err;
    }
  DBG ("Successfully allocated memory for direct I/O buffers");

  // Even if we've successfully opened the file for direct I/O and have
  // allocated our buffer, it's still possible that there's something wrong
  // with our buffer size or alignment.  So do a test read to see if it
  // succeeds, and if so, seek back to the beginning of the file.
  if (read (fd, buf, buf_size) == -1
      || lseek (fd, 0, SEEK_SET) == (off_t) -1
      || (cancellable != NULL && g_cancellable_is_cancelled (cancellable)))
    {
      goto out_err;
    }
  DBG ("Direct I/O is available and seems to be working");

  *buf_out = buf;
  *buf_align_out = buf_align;
  *buf_size_out = buf_size;

  return g_unix_input_stream_new (fd, TRUE);

out_err:

  if (fd >= 0)
    {
      close (fd);
    }
  free (buf);

  return NULL;
}
#endif



static GInputStream *
open_file_and_buffer_fallback (GFile        *file,
                               GCancellable *cancellable,
                               void        **buf_out,
                               unsigned int *buf_align_out,
                               size_t       *buf_size_out,
                               GError      **error)
{
  GFileInputStream *finp;
  void             *buf = NULL;

  finp = g_file_read (file, cancellable, error);

  if (finp != NULL)
    {
      /* Use 16-byte aligned allocations (rather than allocating on the stack), as
         that's usually a requirement for any SIMD optimizations the compiler might
         be able to do. */
      if (posix_memalign (&buf, CMP_BUF_MIN_ALIGN, CMP_BUF_SIZE) != 0)
        {
          g_set_error (error, G_IO_ERROR, g_io_error_from_errno (errno), "Failed to allocate aligned memory: %s", g_strerror (errno));
        }
    }

  if (finp != NULL && buf != NULL)
    {
      *buf_out = buf;
      *buf_align_out = CMP_BUF_MIN_ALIGN;
      *buf_size_out = CMP_BUF_SIZE;

      return G_INPUT_STREAM (finp);
    }
  else
    {
      if (finp != NULL)
        {
          g_object_unref (finp);
        }
      free (buf);

      return FALSE;
    }
}



/**
 * thunar_g_file_compare_contents:
 * @file_a      : a #GFile
 * @file_b      : a #GFile
 * @cancellable : (nullalble): optional #GCancellable object
 * @error       : (nullalble): optional #GError
 *
 * Compares the contents of @file_a and @file_b.
 *
 * Return value: %TRUE if the files match, %FALSE if not.
 **/
gboolean
thunar_g_file_compare_contents (GFile        *file_a,
                                GFile        *file_b,
                                GCancellable *cancellable,
                                GError      **error)
{
  GFileInputStream *inp_a = NULL;
  GInputStream     *inp_b = NULL;
  void             *buf_a = NULL;
  void             *buf_b = NULL;
  unsigned int      buf_align = 0;
  size_t            buf_size = 0;
  gboolean          is_equal = FALSE;

  g_return_val_if_fail (G_IS_FILE (file_a), FALSE);
  g_return_val_if_fail (G_IS_FILE (file_b), FALSE);
  g_return_val_if_fail (cancellable == NULL || G_IS_CANCELLABLE (cancellable), FALSE);
  g_return_val_if_fail (error == NULL || *error == NULL, FALSE);

  // We assume that file_b is the destination file, and that's the one that
  // really needs to be opened using O_DIRECT, as it's just been written, and
  // some (or even all) of it will still be in the kernel's buffer cache.
  // file_a can be opened normally, and will hopefully/likely be read from
  // buffer cache, which is both fine and faster.

  inp_a = g_file_read (file_a, cancellable, error);

  if (inp_a != NULL)
    {
#ifdef HAVE_DIRECT_IO
      if (g_file_has_uri_scheme (file_b, "file"))
        {
          DBG ("Attempting direct I/O for destination file");
          inp_b = open_file_and_buffer_for_direct_io (file_b, cancellable, &buf_b, &buf_align, &buf_size);
        }
#endif

      if (inp_b == NULL)
        {
          inp_b = open_file_and_buffer_fallback (file_b, cancellable, &buf_b, &buf_align, &buf_size, error);
        }
    }

  if (inp_b != NULL)
    {
      // Now that we know the alignment and size needed for file_b, we can also
      // allocate the buffer for file_a.
      if (posix_memalign (&buf_a, buf_align, buf_size) != 0)
        {
          g_set_error (error, G_IO_ERROR, g_io_error_from_errno (errno), "Failed to allocate aligned memory: %s", g_strerror (errno));
        }
    }

  if (inp_a != NULL && inp_b != NULL && buf_a != NULL)
    {
      for (;;)
        {
          gsize bytes_read_a;
          gsize bytes_read_b;

          if (!g_input_stream_read_all (G_INPUT_STREAM (inp_a), buf_a, buf_size, &bytes_read_a, cancellable, error))
            {
              break;
            }
          if (!g_input_stream_read_all (G_INPUT_STREAM (inp_b), buf_b, buf_size, &bytes_read_b, cancellable, error))
            {
              break;
            }

          if (bytes_read_a == 0 && bytes_read_b == 0)
            {
              is_equal = TRUE;
              break;
            }
          else if (bytes_read_a != bytes_read_b)
            {
              break;
            }
          else if (memcmp (buf_a, buf_b, bytes_read_a) != 0)
            {
              break;
            }
        }
    }

  if (inp_a != NULL)
    {
      g_object_unref (inp_a);
    }
  if (inp_b != NULL)
    {
      g_object_unref (inp_b);
    }

  free (buf_a);
  free (buf_b);

  return is_equal;
}



/**
 * thunar_g_file_list_new_from_string:
 * @string : a string representation of a URI list.
 *
 * Splits a URI list conforming to the text/uri-list
 * mime type defined in RFC 2483 into individual URIs,
 * discarding any comments and whitespace. The resulting
 * list will hold one #GFile for each URI.
 *
 * If @string contains no URIs, this function
 * will return %NULL.
 *
 * Return value: the list of #GFile<!---->s or %NULL.
 **/
GList *
thunar_g_file_list_new_from_string (const gchar *string)
{
  GList  *list = NULL;
  gchar **uris;
  gsize   n;

  uris = g_uri_list_extract_uris (string);

  for (n = 0; uris != NULL && uris[n] != NULL; ++n)
    list = g_list_append (list, g_file_new_for_uri (uris[n]));

  g_strfreev (uris);

  return list;
}



/**
 * thunar_g_file_list_to_stringv:
 * @list : a list of #GFile<!---->s.
 *
 * Free the returned value using g_strfreev() when you
 * are done with it. Useful for gtk_selection_data_set_uris.
 *
 * Return value: and array of uris.
 **/
gchar **
thunar_g_file_list_to_stringv (GList *list)
{
  gchar **uris;
  guint   n;
  GList  *lp;

  /* allocate initial string */
  uris = g_new0 (gchar *, g_list_length (list) + 1);

  for (lp = list, n = 0; lp != NULL; lp = lp->next)
    {
      /* Prefer native paths for interoperability. */
      gchar *path = g_file_get_path (G_FILE (lp->data));
      if (path == NULL)
        {
          uris[n++] = g_file_get_uri (G_FILE (lp->data));
        }
      else
        {
          uris[n++] = g_filename_to_uri (path, NULL, NULL);
          g_free (path);
        }
    }

  return uris;
}



/**
 * thunar_g_file_list_get_parents:
 * @list : a list of #GFile<!---->s.
 *
 * Collects all parent folders of the passed files
 * If multiple files share the same parent, the parent will only be added once to the returned list.
 * Each list element of the returned list needs to be freed with g_object_unref() after use.
 *
 * Return value: A list of #GFile<!---->s of all parent folders. Free the returned list with calling g_object_unref() on each element
 **/
GList *
thunar_g_file_list_get_parents (GList *file_list)
{
  GList   *lp_file_list;
  GList   *lp_parent_folder_list;
  GFile   *parent_folder;
  GList   *parent_folder_list = NULL;
  gboolean folder_already_added;

  for (lp_file_list = file_list; lp_file_list != NULL; lp_file_list = lp_file_list->next)
    {
      if (!G_IS_FILE (lp_file_list->data))
        continue;
      parent_folder = g_file_get_parent (lp_file_list->data);
      if (parent_folder == NULL)
        continue;
      folder_already_added = FALSE;
      /* Check if the folder already is in our list */
      for (lp_parent_folder_list = parent_folder_list; lp_parent_folder_list != NULL; lp_parent_folder_list = lp_parent_folder_list->next)
        {
          if (g_file_equal (lp_parent_folder_list->data, parent_folder))
            {
              folder_already_added = TRUE;
              break;
            }
        }
      /* Keep the reference for each folder added to parent_folder_list */
      if (folder_already_added)
        g_object_unref (parent_folder);
      else
        parent_folder_list = g_list_append (parent_folder_list, parent_folder);
    }
  return parent_folder_list;
}



/**
 * thunar_g_file_is_descendant:
 * @descendant : a #GFile that is a potential descendant of @ancestor
 * @ancestor   : a #GFile
 *
 * Check if @descendant is a subdirectory of @ancestor.
 * @descendant == @ancestor also counts.
 *
 * Returns: %TRUE if @descendant is a subdirectory of @ancestor.
 **/
gboolean
thunar_g_file_is_descendant (GFile *descendant,
                             GFile *ancestor)
{
  _thunar_return_val_if_fail (descendant != NULL && G_IS_FILE (descendant), FALSE);
  _thunar_return_val_if_fail (ancestor != NULL && G_IS_FILE (ancestor), FALSE);

  for (GFile *parent = g_object_ref (descendant), *temp;
       parent != NULL;
       temp = g_file_get_parent (parent), g_object_unref (parent), parent = temp)
    {
      if (g_file_equal (parent, ancestor))
        {
          g_object_unref (parent);
          return TRUE;
        }
    }

  return FALSE;
}



gboolean
thunar_g_app_info_launch (GAppInfo          *info,
                          GFile             *working_directory,
                          GList             *path_list,
                          GAppLaunchContext *context,
                          GError           **error)
{
  ThunarFile  *file;
  GAppInfo    *default_app_info;
  GList       *recommended_app_infos;
  GList       *lp;
  const gchar *content_type;
  gboolean     result = FALSE;
  gchar       *new_path = NULL;
  gchar       *old_path = NULL;
  gboolean     skip_app_info_update;

  _thunar_return_val_if_fail (G_IS_APP_INFO (info), FALSE);
  _thunar_return_val_if_fail (working_directory == NULL || G_IS_FILE (working_directory), FALSE);
  _thunar_return_val_if_fail (path_list != NULL, FALSE);
  _thunar_return_val_if_fail (G_IS_APP_LAUNCH_CONTEXT (context), FALSE);
  _thunar_return_val_if_fail (error == NULL || *error == NULL, FALSE);

  skip_app_info_update = (g_object_get_data (G_OBJECT (info), "skip-app-info-update") != NULL);

  /* check if we want to set the working directory of the spawned app */
  if (working_directory != NULL)
    {
      /* determine the working directory path */
      new_path = g_file_get_path (working_directory);
      if (new_path != NULL)
        {
          /* switch to the desired working directory, remember that of Thunar itself */
          old_path = thunar_util_change_working_directory (new_path);

          /* forget about the new working directory path */
          g_free (new_path);
        }
    }

  /* launch the paths with the specified app info */
  result = g_app_info_launch (info, path_list, context, error);

  /* if successful, remember the application as last used for the file types */
  if (result == TRUE)
    {
      for (lp = path_list; lp != NULL; lp = lp->next)
        {
          gboolean update_app_info = !skip_app_info_update;

          file = thunar_file_get (lp->data, NULL);
          if (file == NULL)
            continue;

          content_type = thunar_file_get_content_type (file);

          /* determine default application */
          default_app_info = thunar_file_get_default_handler (file);
          if (default_app_info != NULL)
            {
              /* check if the application is the default one */
              if (g_app_info_equal (info, default_app_info))
                update_app_info = FALSE;
              g_object_unref (default_app_info);
            }

          if (update_app_info)
            {
              /* obtain list of last used applications */
              recommended_app_infos = g_app_info_get_recommended_for_type (content_type);
              if (recommended_app_infos != NULL)
                {
                  /* check if the application is already the last used one
                   * by comparing it with the first entry in the list */
                  if (g_app_info_equal (info, recommended_app_infos->data))
                    update_app_info = FALSE;

                  g_list_free_full (recommended_app_infos, g_object_unref);
                }
            }

          /* emit "changed" on the file if we successfully changed the last used application */
          if (update_app_info && g_app_info_set_as_last_used_for_type (info, content_type, NULL))
            thunar_file_changed (file);

          g_object_unref (file);
        }
    }

  /* check if we need to reset the working directory to the one Thunar was
   * opened from */
  if (old_path != NULL)
    {
      /* switch to Thunar's original working directory */
      new_path = thunar_util_change_working_directory (old_path);

      /* clean up */
      g_free (new_path);
      g_free (old_path);
    }

  return result;
}



gboolean
thunar_g_app_info_should_show (GAppInfo *info)
{
#ifdef HAVE_GIO_UNIX
  _thunar_return_val_if_fail (G_IS_APP_INFO (info), FALSE);

  if (G_IS_DESKTOP_APP_INFO (info))
    {
      /* NoDisplay=true files should be visible in the interface,
       * because this key is intent to hide mime-helpers from the
       * application menu. Hidden=true is never returned by GIO. */
      return g_desktop_app_info_get_show_in (G_DESKTOP_APP_INFO (info), NULL);
    }

  return TRUE;
#else
  /* we cannot exclude custom actions, so show everything */
  return TRUE;
#endif
}



gboolean
thunar_g_vfs_metadata_is_supported (void)
{
  GFile                  *root;
  GFileAttributeInfoList *attr_info_list;
  gint                    n;
  gboolean                metadata_is_supported = FALSE;

  /* get a GFile for the root directory, and obtain the list of writable namespaces for it */
  root = thunar_g_file_new_for_root ();
  attr_info_list = g_file_query_writable_namespaces (root, NULL, NULL);
  g_object_unref (root);

  /* loop through the returned namespace names and check if "metadata" is included */
  for (n = 0; n < attr_info_list->n_infos; n++)
    {
      if (g_strcmp0 (attr_info_list->infos[n].name, "metadata") == 0)
        {
          metadata_is_supported = TRUE;
          break;
        }
    }

  /* release the attribute info list */
  g_file_attribute_info_list_unref (attr_info_list);

  return metadata_is_supported;
}



/**
 * thunar_g_file_is_on_local_device:
 * @file : the source or target #GFile to test.
 *
 * Tries to find if the @file is on a local device or not.
 * Local device if (all conditions should match):
 * - the file has a 'file' uri scheme.
 * - the file is located on devices not handled by the #GVolumeMonitor (GVFS).
 * - the device is handled by #GVolumeMonitor (GVFS) and cannot be unmounted
 *   (USB key/disk, fuse mounts, Samba shares, PTP devices).
 *
 * The target @file may not exist yet when this function is used, so recurse
 * the parent directory, possibly reaching the root mountpoint.
 *
 * This should be enough to determine if a @file is on a local device or not.
 *
 * Return value: %TRUE if #GFile @file is on a so-called local device.
 **/
gboolean
thunar_g_file_is_on_local_device (GFile *file)
{
  gboolean is_local = FALSE;
  GFile   *target_file;
  GFile   *target_parent;
  GMount  *file_mount;

  _thunar_return_val_if_fail (file != NULL, TRUE);
  _thunar_return_val_if_fail (G_IS_FILE (file), TRUE);

  if (g_file_has_uri_scheme (file, "file") == FALSE)
    return FALSE;
  for (target_file = g_object_ref (file);
       target_file != NULL;
       target_file = target_parent)
    {
      if (g_file_query_exists (target_file, NULL))
        break;

      /* file or parent directory does not exist (yet)
       * query the parent directory */
      target_parent = g_file_get_parent (target_file);
      g_object_unref (target_file);
    }

  if (target_file == NULL)
    return FALSE;

  /* file_mount will be NULL for local files on local partitions/devices */
  file_mount = g_file_find_enclosing_mount (target_file, NULL, NULL);
  g_object_unref (target_file);
  if (file_mount == NULL)
    is_local = TRUE;
  else
    {
      /* mountpoints which cannot be unmounted are local devices.
       * attached devices like USB key/disk, fuse mounts, Samba shares,
       * PTP devices can always be unmounted and are considered remote/slow. */
      is_local = !g_mount_can_unmount (file_mount);
      g_object_unref (file_mount);
    }

  return is_local;
}



/**
 * thunar_g_file_set_executable_flags:
 * @file : the #GFile for which execute flags should be set
 *
 * Tries to set +x flag of the file for user, group and others
 *
 * Return value: %TRUE on sucess, %FALSE on error
 **/
gboolean
thunar_g_file_set_executable_flags (GFile   *file,
                                    GError **error)
{
  ThunarFileMode old_mode;
  ThunarFileMode new_mode;
  GFileInfo     *info;

  _thunar_return_val_if_fail (G_IS_FILE (file), FALSE);
  _thunar_return_val_if_fail (error == NULL || *error == NULL, FALSE);

  /* try to query information about the file */
  info = g_file_query_info (file,
                            G_FILE_ATTRIBUTE_UNIX_MODE,
                            G_FILE_QUERY_INFO_NOFOLLOW_SYMLINKS,
                            NULL, error);

  if (G_LIKELY (info != NULL))
    {
      if (g_file_info_has_attribute (info, G_FILE_ATTRIBUTE_UNIX_MODE))
        {
          /* determine the current mode */
          old_mode = g_file_info_get_attribute_uint32 (info, G_FILE_ATTRIBUTE_UNIX_MODE);

          /* generate the new mode */
          new_mode = old_mode | THUNAR_FILE_MODE_USR_EXEC | THUNAR_FILE_MODE_GRP_EXEC | THUNAR_FILE_MODE_OTH_EXEC;

          if (old_mode != new_mode)
            {
              g_file_set_attribute_uint32 (file,
                                           G_FILE_ATTRIBUTE_UNIX_MODE, new_mode,
                                           G_FILE_QUERY_INFO_NOFOLLOW_SYMLINKS,
                                           NULL, error);
            }
        }
      else
        {
          g_warning ("No %s attribute found", G_FILE_ATTRIBUTE_UNIX_MODE);
        }

      g_object_unref (info);
    }

  return (error == NULL);
}



/**
 * thunar_g_file_is_in_xdg_data_dir:
 * @file      : a #GFile.
 *
 * Returns %TRUE if @file is located below one of the directories given in XDG_DATA_DIRS
 *
 * Return value: %TRUE if @file is located inside a XDG_DATA_DIR
 **/
gboolean
thunar_g_file_is_in_xdg_data_dir (GFile *file)
{
  const gchar *const *data_dirs;
  guint               i;
  gchar              *path;
  gboolean            found = FALSE;

  _thunar_return_val_if_fail (G_IS_FILE (file), FALSE);

  if (g_file_is_native (file))
    {
      data_dirs = g_get_system_data_dirs ();
      if (G_LIKELY (data_dirs != NULL))
        {
          path = g_file_get_path (file);
          for (i = 0; data_dirs[i] != NULL; i++)
            {
              if (g_str_has_prefix (path, data_dirs[i]))
                {
                  found = TRUE;
                  break;
                }
            }
          g_free (path);
        }
    }
  return found;
}



/**
 * thunar_g_file_is_desktop_file:
 * @file      : a #GFile.
 *
 * Returns %TRUE if @file is a .desktop file.
 *
 * Return value: %TRUE if @file is a .desktop file.
 **/
gboolean
thunar_g_file_is_desktop_file (GFile *file)
{
  gchar     *basename;
  gboolean   is_desktop_file = FALSE;
  GFileInfo *info;

  _thunar_return_val_if_fail (G_IS_FILE (file), FALSE);

  basename = g_file_get_basename (file);

  /* only allow regular files with a .desktop extension */
  if (g_str_has_suffix (basename, ".desktop"))
    {
      info = g_file_query_info (file, G_FILE_ATTRIBUTE_STANDARD_TYPE, G_FILE_QUERY_INFO_NOFOLLOW_SYMLINKS, NULL, NULL);
      if (G_LIKELY (info != NULL && g_file_info_get_file_type (info) == G_FILE_TYPE_REGULAR))
        is_desktop_file = TRUE;
      g_object_unref (info);
    }

  g_free (basename);
  return is_desktop_file;
}



/**
 * thunar_g_file_get_link_path_for_symlink:
 * @file_to_link : the #GFile to which @symlink will have to point
 * @symlink      : a #GFile representing the symlink
 *
 * Method to build the link target path in order to link from @symlink to @file_to_link.
 * The caller is responsible for freeing the string using g_free() when done.
 *
 * Return value: The link path, or NULL on failure
 **/
char *
thunar_g_file_get_link_path_for_symlink (GFile *file_to_link,
                                         GFile *symlink)
{
  GFile *root;
  GFile *parent;
  char  *relative_path;
  char  *link_path;

  _thunar_return_val_if_fail (G_IS_FILE (file_to_link), NULL);
  _thunar_return_val_if_fail (G_IS_FILE (symlink), NULL);

  /* */
  if (g_file_is_native (file_to_link) || g_file_is_native (symlink))
    {
      return g_file_get_path (file_to_link);
    }

  /* Search for the filesystem root */
  root = g_object_ref (file_to_link);
  while ((parent = g_file_get_parent (root)) != NULL)
    {
      g_object_unref (root);
      root = parent;
    }

  /* Build a absolute path, using the relative path up to the filesystem root */
  relative_path = g_file_get_relative_path (root, file_to_link);
  g_object_unref (root);
  link_path = g_strconcat ("/", relative_path, NULL);
  g_free (relative_path);
  return link_path;
}



static GFileInfo *
thunar_g_file_get_content_type_query_info (GFile   *gfile,
                                           GError **err)
{
  GFileInfo *info = NULL;

  info = g_file_query_info (gfile,
                            G_FILE_ATTRIBUTE_STANDARD_TYPE "," G_FILE_ATTRIBUTE_STANDARD_IS_SYMLINK "," G_FILE_ATTRIBUTE_STANDARD_CONTENT_TYPE "," G_FILE_ATTRIBUTE_STANDARD_FAST_CONTENT_TYPE,
                            G_FILE_QUERY_INFO_NONE,
                            NULL, err);
  return info;
}



/**
 * thunar_g_file_get_content_type:
 * @file : #GFile for which the content type will be returned
 *
 * Gets the content type of the passed #GFile. Will always return a valid string.
 *
 * Return value: (transfer full): The content type as #gchar
 * The returned string should be freed with g_free() when no longer needed.
 **/
char *
thunar_g_file_get_content_type (GFile *gfile)
{
  gboolean   is_symlink = FALSE;
  GFileInfo *info = NULL;
  GError    *err = NULL;
  gchar     *content_type = NULL;

  info = thunar_g_file_get_content_type_query_info (gfile, &err);

  /* follow symlink, if found */
  if (G_LIKELY (info != NULL))
    {
      is_symlink = g_file_info_get_attribute_boolean (info, G_FILE_ATTRIBUTE_STANDARD_IS_SYMLINK);
      if (is_symlink)
        {
          GFile *link_target = thunar_g_file_resolve_symlink (gfile);
          g_object_unref (info);
          info = NULL;
          if (link_target != NULL)
            {
              info = thunar_g_file_get_content_type_query_info (link_target, &err);
              g_object_unref (link_target);
            }
        }
    }

  if (G_LIKELY (info != NULL))
    {
      if (g_file_info_get_file_type (info) == G_FILE_TYPE_DIRECTORY)
        {
          /* this we known for sure */
          content_type = g_strdup ("inode/directory");
        }
      else
        {
          content_type = g_strdup (g_file_info_get_attribute_string (info, G_FILE_ATTRIBUTE_STANDARD_CONTENT_TYPE));
          if (G_UNLIKELY (content_type == NULL))
            content_type = g_strdup (g_file_info_get_attribute_string (info, G_FILE_ATTRIBUTE_STANDARD_FAST_CONTENT_TYPE));
        }

      g_object_unref (info);
    }
  else
    {
      /* If gfile retrieved above is NULL, then g_file_query_info won't be called, thus keeping info NULL.
       * In this case, err will also be NULL. So it will fallback to "unknown" mime-type */
      if (G_LIKELY (err != NULL))
        {
          /* The mime-type 'inode/symlink' is  only used for broken links.
           * When the link is functional, the mime-type of the link target will be used */
          if (G_LIKELY (is_symlink && err->code == G_IO_ERROR_NOT_FOUND))
            content_type = g_strdup ("inode/symlink");
          else
            {
              gchar *uri = g_file_get_uri (gfile);
              g_warning ("Content type loading failed for %s: %s",
                         uri,
                         err->message);
              g_free (uri);
            }
          g_error_free (err);
        }
    }

  /* fallback */
  if (content_type == NULL)
    content_type = g_strdup (DEFAULT_CONTENT_TYPE);

  return content_type;
}



static void
thunar_g_file_set_metadata_setting_finish (GObject      *source_object,
                                           GAsyncResult *result,
                                           gpointer      user_data)
{
  GFile      *file = NULL;
  GError     *error = NULL;
  ThunarFile *thunar_file = NULL;

  file = G_FILE (source_object);

  if (file == NULL)
    return;

  if (!g_file_set_attributes_finish (file, result, NULL, &error))
    {
      g_warning ("Failed to set metadata: %s", error->message);
      g_error_free (error);
    }

  thunar_file = thunar_file_get (file, NULL);

  if (thunar_file == NULL)
    return;

  thunar_file_changed (thunar_file);
  g_object_unref (thunar_file);
}



static void
thunar_g_file_info_set_attribute (GFileInfo   *info,
                                  ThunarGType  type,
                                  const gchar *setting_name,
                                  const gchar *setting_value)
{
  switch (type)
    {
    case THUNAR_GTYPE_STRING:
      g_file_info_set_attribute_string (info, setting_name, setting_value);
      break;

    case THUNAR_GTYPE_STRINGV:
      {
        gchar **setting_values;
        setting_values = g_strsplit (setting_value, THUNAR_METADATA_STRING_DELIMETER, 100);
        g_file_info_set_attribute_stringv (info, setting_name, setting_values);
        g_strfreev (setting_values);
        break;
      }

    default:
      g_warning ("ThunarGType not supported, skipping");
      break;
    }
}



static gchar *
thunar_g_file_info_get_attribute (GFileInfo   *info,
                                  ThunarGType  type,
                                  const gchar *setting_name)
{
  switch (type)
    {
    case THUNAR_GTYPE_STRING:
      return g_strdup (g_file_info_get_attribute_string (info, setting_name));

    case THUNAR_GTYPE_STRINGV:
      {
        gchar **stringv = g_file_info_get_attribute_stringv (info, setting_name);
        GList  *string_list = NULL;
        gchar  *joined_string = NULL;

        if (stringv == NULL)
          return NULL;

        for (; *stringv != NULL; ++stringv)
          string_list = g_list_append (string_list, *stringv);

        joined_string = thunar_util_strjoin_list (string_list, THUNAR_METADATA_STRING_DELIMETER);
        g_list_free (string_list);
        return joined_string;
      }

    default:
      g_warning ("ThunarGType not supported, skipping");
      return NULL;
    }
}



/**
 * thunar_g_file_set_metadata_setting:
 * @file          : a #GFile instance.
 * @info          : Additional #GFileInfo instance to update
 * @type          : #ThunarGType of the metadata
 * @setting_name  : the name of the setting to set
 * @setting_value : the value to set
 * @async         : whether g_file_set_attributes_async or g_file_set_attributes_from_info should be used
 *
 * Sets the setting @setting_name of @file to @setting_value and stores it in
 * the @file<!---->s metadata.
 **/
void
thunar_g_file_set_metadata_setting (GFile       *file,
                                    GFileInfo   *info,
                                    ThunarGType  type,
                                    const gchar *setting_name,
                                    const gchar *setting_value,
                                    gboolean     async)
{
  GFileInfo *info_new;
  gchar     *attr_name;

  _thunar_return_if_fail (G_IS_FILE (file));
  _thunar_return_if_fail (G_IS_FILE_INFO (info));

  /* convert the setting name to an attribute name */
  attr_name = g_strdup_printf ("metadata::%s", setting_name);

  /* set the value in the current info. this call is needed to update the in-memory
   * GFileInfo structure to ensure that the new attribute value is available immediately */
  if (setting_value)
    thunar_g_file_info_set_attribute (info, type, attr_name, setting_value);

  /* send meta data to the daemon. this call is needed to store the new value of
   * the attribute in the file system */
  info_new = g_file_info_new ();
  thunar_g_file_info_set_attribute (info_new, type, attr_name, setting_value);
  if (async)
    {
      g_file_set_attributes_async (file, info_new,
                                   G_FILE_QUERY_INFO_NONE,
                                   G_PRIORITY_DEFAULT,
                                   NULL,
                                   thunar_g_file_set_metadata_setting_finish,
                                   NULL);
    }
  else
    {
      g_file_set_attributes_from_info (file, info_new,
                                       G_FILE_QUERY_INFO_NONE,
                                       NULL,
                                       NULL);
    }
  g_free (attr_name);
  g_object_unref (G_OBJECT (info_new));
}



/**
 * thunar_file_clear_metadata_setting:
 * @file          : a #GFile instance.
 * @setting_name  : the name of the setting to clear
 *
 * Clear the metadata setting @setting_name of @file
 **/
void
thunar_g_file_clear_metadata_setting (GFile       *file,
                                      GFileInfo   *info,
                                      const gchar *setting_name)
{
  gchar *attr_name;

  _thunar_return_if_fail (G_IS_FILE (file));

  if (info == NULL)
    return;

  /* convert the setting name to an attribute name */
  attr_name = g_strdup_printf ("metadata::%s", setting_name);

  if (!g_file_info_has_attribute (info, attr_name))
    {
      g_free (attr_name);
      return;
    }

  g_file_info_remove_attribute (info, attr_name);
  g_file_set_attribute (file, attr_name, G_FILE_ATTRIBUTE_TYPE_INVALID,
                        NULL, G_FILE_QUERY_INFO_NONE, NULL, NULL);
  g_free (attr_name);
}



/**
 * thunar_file_get_metadata_setting:
 * @file         : a #GFile instance.
 * @info         : #GFileInfo instance to extract the setting
 * @type         : #ThunarGType of the metadata
 * @setting_name : the name of the setting to get
 *
 * Gets the stored value of the metadata setting @setting_name for @file. Returns %NULL
 * if there is no stored setting. After usage the returned string needs to be released with 'g_free'
 *
 * Return value: (transfer full): the stored value of the setting for @file, or %NULL
 **/
gchar *
thunar_g_file_get_metadata_setting (GFile       *file,
                                    GFileInfo   *info,
                                    ThunarGType  type,
                                    const gchar *setting_name)
{
  gchar *attr_name;
  gchar *attr_value;

  _thunar_return_val_if_fail (G_IS_FILE (file), NULL);

  if (info == NULL)
    return NULL;

  /* convert the setting name to an attribute name */
  attr_name = g_strdup_printf ("metadata::%s", setting_name);

  if (!g_file_info_has_attribute (info, attr_name))
    {
      g_free (attr_name);
      return NULL;
    }

  attr_value = thunar_g_file_info_get_attribute (info, type, attr_name);
  g_free (attr_name);

  return attr_value;
}



/**
 * thunar_g_update_user_special_dir:
 * @file     : a #GFile.
 * @xdg_name : the XDG name of the directory
 *
 * Sets the XDG special user directory of @xdg_name to @file path
 * and reloads the cache.
 */
void
thunar_g_update_user_special_dir (GFile       *file,
                                  const gchar *xdg_name)
{
  gchar *path;
  gchar *command;

  _thunar_return_if_fail (G_IS_FILE (file));

  path = g_file_get_path (file);
  if (path == NULL)
    return;

  command = g_strdup_printf ("xdg-user-dirs-update --set %s \"%s\"",
                             xdg_name, path);
  if (g_spawn_command_line_sync (command, NULL, NULL, NULL, NULL))
    {
      /* reload the cache to reflect changes */
      g_reload_user_special_dirs_cache ();
    }
  else
    {
      g_warning ("Failed to run xdg-user-dirs-update for %s", xdg_name);
    }

  g_free (command);
  g_free (path);
}



/**
 * thunar_g_file_fs_uses_fat_name_scheme:
 * @file     : a #GFile.
 *
 * Returns TRUE, if the filesystem of this file requires usage of FAT naming scheme, FALSE otherwise
 */
gboolean
thunar_g_file_fs_uses_fat_name_scheme (GFile *gfile)
{
  gboolean use_fat_name_scheme = FALSE; /* default */
  g_autoptr (GFile) parent = NULL;
  g_autoptr (GFileInfo) info = NULL;

  _thunar_return_val_if_fail (G_IS_FILE (gfile), FALSE);

  parent = g_file_get_parent (gfile);
  if (parent == NULL)
    {
      gchar *uri = g_file_get_uri (gfile);
      g_warning ("Failed to get parent of %s", uri);
      g_free (uri);
      return FALSE;
    }

  info = g_file_query_filesystem_info (parent,
                                       G_FILE_ATTRIBUTE_FILESYSTEM_TYPE,
                                       NULL, NULL);
  if (info != NULL)
    {
      const char *fs_type = g_file_info_get_attribute_string (info, G_FILE_ATTRIBUTE_FILESYSTEM_TYPE);

      /* clang-format off */
      use_fat_name_scheme =
        !g_strcmp0 (fs_type, "fat")   ||
        !g_strcmp0 (fs_type, "vfat")  ||
        !g_strcmp0 (fs_type, "exfat") ||
        !g_strcmp0 (fs_type, "fuse")  ||
        !g_strcmp0 (fs_type, "ntfs")  ||
        !g_strcmp0 (fs_type, "msdos") ||
        !g_strcmp0 (fs_type, "msdosfs");
      /* clang-format on */
    }

  return use_fat_name_scheme;
}



/**
 * thunar_g_file_transform_to_fat_name_scheme:
 * @file     : a #GFile.
 *
 * Creates a new GFile which has the same URI as @file, but a basename which fulfills the FAT32 naming scheme.
 * The returned file neds to be released with g_object_unref after usage.
 *
 * Return value: (transfer full): A new, possibly renamed GFile, which fulfills the fat naming scheme, or %NULL
 */
GFile *
thunar_g_file_transform_to_fat_name_scheme (GFile *gfile)
{
  static const GRegex *windows_reserved_name = NULL;
  g_autoptr (GFile) parent = NULL;
  g_autofree gchar *base_name = NULL;

  _thunar_return_val_if_fail (G_IS_FILE (gfile), NULL);

  /* if regex pattern is not initialized, do it */
  if (G_UNLIKELY (windows_reserved_name == NULL))
    {
      /* COM#, LPT#, CON, PRN, AUX, and NUL are not allowed */
      /* FAT is case-insensitive by default */
      windows_reserved_name = g_regex_new ("^((COM\\d)|(LPT\\d)|(CON)|(PRN)|(AUX)|(NUL))(\\..*)?$", G_REGEX_CASELESS, 0, NULL);
    }

  parent = g_file_get_parent (gfile);

  if (parent == NULL)
    return NULL;

  base_name = g_file_get_basename (gfile);

  /* replace character which are invalid for FAT filenames */
  g_strdelimit (g_strchomp (base_name), "/:*?\"<>\\|", '_');

  /* ASCII characters 0~31 are as well invalid for FAT filenames */
  for (int i = 0; base_name[i] != '\0'; i++)
    {
      if (base_name[i] >= 0 && base_name[i] < 32)
        base_name[i] = '_';
    }

  /* avoid FAT reserved names */
  if (g_regex_match (windows_reserved_name, base_name, 0, NULL))
    {
      g_autofree gchar *tmp = base_name;
      base_name = g_strconcat ("__", tmp, NULL);
    }

  /* avoid filename that ends with '.' */
  if (g_str_has_suffix (base_name, "."))
    {
      g_autofree gchar *tmp = base_name;
      base_name = g_strconcat (tmp, "___", NULL);
    }

  return g_file_get_child (parent, base_name);
}
