/* GIO - GLib Input, Output and Streaming Library
 * 
 * Copyright (C) 2006-2007 Red Hat, Inc.
 *
 * This library is free software; you can redistribute it and/or
 * modify it under the terms of the GNU Lesser General Public
 * License as published by the Free Software Foundation; either
 * version 2 of the License, or (at your option) any later version.
 *
 * This library 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
 * Lesser General Public License for more details.
 *
 * You should have received a copy of the GNU Lesser General
 * Public License along with this library; if not, write to the
 * Free Software Foundation, Inc., 59 Temple Place, Suite 330,
 * Boston, MA 02111-1307, USA.
 *
 * Author: Alexander Larsson <alexl@redhat.com>
 */

#include <config.h>

#include <sys/types.h>
#include <sys/stat.h>
#include <errno.h>
#include <unistd.h>
#include <fcntl.h>
#include <string.h>
#include <stdlib.h>

#include <glib/gstdio.h>
#include <glib/gi18n.h>
#include <gio/gio.h>

#include "gvfsbackendsmb.h"
#include "gvfsjobopenforread.h"
#include "gvfsjobread.h"
#include "gvfsjobseekread.h"
#include "gvfsjobopenforwrite.h"
#include "gvfsjobwrite.h"
#include "gvfsjobclosewrite.h"
#include "gvfsjobseekwrite.h"
#include "gvfsjobsetdisplayname.h"
#include "gvfsjobqueryinfo.h"
#include "gvfsjobqueryfsinfo.h"
#include "gvfsjobqueryattributes.h"
#include "gvfsjobenumerate.h"
#include "gvfsdaemonprotocol.h"
#include "gvfskeyring.h"

#include <libsmbclient.h>
#include "libsmb-compat.h"

struct _GVfsBackendSmb
{
  GVfsBackend parent_instance;

  char *server;
  char *share;
  char *user;
  char *domain;
  char *path;
  char *default_workgroup;
  
  SMBCCTX *smb_context;

  char *last_user;
  char *last_domain;
  char *last_password;
  
  GMountSource *mount_source; /* Only used/set during mount */
  int mount_try;
  gboolean mount_try_again;
  gboolean mount_cancelled;
	
  gboolean password_in_keyring;
  GPasswordSave password_save;
  
  /* Cache */
  char *cached_server_name;
  char *cached_share_name;
  char *cached_domain;
  char *cached_username;
  SMBCSRV *cached_server;
};


G_DEFINE_TYPE (GVfsBackendSmb, g_vfs_backend_smb, G_VFS_TYPE_BACKEND)

static void set_info_from_stat (GVfsBackendSmb *backend,
				GFileInfo *info,
				struct stat *statbuf,
				const char *basename,
				GFileAttributeMatcher *matcher);


static void
g_vfs_backend_smb_finalize (GObject *object)
{
  GVfsBackendSmb *backend;

  backend = G_VFS_BACKEND_SMB (object);

  g_free (backend->share);
  g_free (backend->server);
  g_free (backend->user);
  g_free (backend->domain);
  g_free (backend->path);
  g_free (backend->default_workgroup);
  
  if (G_OBJECT_CLASS (g_vfs_backend_smb_parent_class)->finalize)
    (*G_OBJECT_CLASS (g_vfs_backend_smb_parent_class)->finalize) (object);
}

static void
g_vfs_backend_smb_init (GVfsBackendSmb *backend)
{
  char *workgroup;
  GSettings *settings;

  /* Get default workgroup name */
  settings = g_settings_new ("org.gnome.system.smb");

  workgroup = g_settings_get_string (settings, "workgroup");
  if (workgroup && workgroup[0])
    backend->default_workgroup = workgroup;
  else
    g_free (workgroup);

  g_object_unref (settings);
}

/**
 * Authentication callback function type (method that includes context)
 * 
 * Type for the the authentication function called by the library to
 * obtain authentication credentals
 *
 * @param context   Pointer to the smb context
 * @param srv       Server being authenticated to
 * @param shr       Share being authenticated to
 * @param wg        Pointer to buffer containing a "hint" for the
 *                  workgroup to be authenticated.  Should be filled in
 *                  with the correct workgroup if the hint is wrong.
 * @param wglen     The size of the workgroup buffer in bytes
 * @param un        Pointer to buffer containing a "hint" for the
 *                  user name to be use for authentication. Should be
 *                  filled in with the correct workgroup if the hint is
 *                  wrong.
 * @param unlen     The size of the username buffer in bytes
 * @param pw        Pointer to buffer containing to which password 
 *                  copied
 * @param pwlen     The size of the password buffer in bytes
 *           
 */
static void
auth_callback (SMBCCTX *context,
	       const char *server_name, const char *share_name,
	       char *domain_out, int domainmaxlen,
	       char *username_out, int unmaxlen,
	       char *password_out, int pwmaxlen)
{
  GVfsBackendSmb *backend;
  char *ask_password, *ask_user, *ask_domain;
  gboolean handled, abort;

  backend = smbc_getOptionUserData (context);

  strncpy (password_out, "", pwmaxlen);
  
  if (backend->domain)
    strncpy (domain_out, backend->domain, domainmaxlen);
  if (backend->user)
    strncpy (username_out, backend->user, unmaxlen);

  if (backend->mount_cancelled)
    {
      /*  Don't prompt for credentials, let smbclient finish the mount loop  */
      strncpy (username_out, "ABORT", unmaxlen);
      strncpy (password_out, "", pwmaxlen);
      return;
    }

  if (backend->mount_source == NULL)
    {
      /* Not during mount, use last password */
      if (backend->last_user)
	strncpy (username_out, backend->last_user, unmaxlen);
      if (backend->last_domain)
	strncpy (domain_out, backend->last_domain, domainmaxlen);
      if (backend->last_password)
	strncpy (password_out, backend->last_password, pwmaxlen);
      
      return;
    }
  
  if (backend->mount_try == 0 &&
      backend->user == NULL &&
      backend->domain == NULL)
    {
      /* Try again if kerberos login + anonymous fallback fails */
      backend->mount_try_again = TRUE;
    }
  else
    {
      gboolean in_keyring = FALSE;

      if (!backend->password_in_keyring)
        {
	  in_keyring = g_vfs_keyring_lookup_password (backend->user,
						      backend->server,
						      backend->domain,
						      "smb",
						      NULL,
						      NULL,
						      0,
						      &ask_user,
						      &ask_domain,
						      &ask_password);
	  backend->password_in_keyring = in_keyring;
	}
      
      if (!in_keyring)
        {
	  GAskPasswordFlags flags = G_ASK_PASSWORD_NEED_PASSWORD;
	  char *message;
      
	  if (g_vfs_keyring_is_available ())
	    flags |= G_ASK_PASSWORD_SAVING_SUPPORTED;
	  if (backend->domain == NULL)
	    flags |= G_ASK_PASSWORD_NEED_DOMAIN;
	  if (backend->user == NULL)
	    flags |= G_ASK_PASSWORD_NEED_USERNAME;

	  /* translators: First %s is a share name, second is a server name */
	  message = g_strdup_printf (_("Password required for share %s on %s"),
				     share_name, server_name);
	  handled = g_mount_source_ask_password (backend->mount_source,
						 message,
						 username_out,
						 domain_out,
						 flags,
						 &abort,
						 &ask_password,
						 &ask_user,
						 &ask_domain,
						 NULL,
						 &(backend->password_save));
	  g_free (message);
	  if (!handled)
	    goto out;
      
	  if (abort)
	    {
	      strncpy (username_out, "ABORT", unmaxlen);
	      strncpy (password_out, "", pwmaxlen);
	      backend->mount_cancelled = TRUE;
	      goto out;
	    }
	}

      /* Try again if this fails */
      backend->mount_try_again = TRUE;

      strncpy (password_out, ask_password, pwmaxlen);
      if (ask_user && *ask_user)
	strncpy (username_out, ask_user, unmaxlen);
      if (ask_domain && *ask_domain)
	strncpy (domain_out, ask_domain, domainmaxlen);

    out:
      g_free (ask_password);
      g_free (ask_user);
      g_free (ask_domain);
    }

  backend->last_user = g_strdup (username_out);
  backend->last_domain = g_strdup (domain_out);
  backend->last_password = g_strdup (password_out);
}

/* Add a server to the cache system
 *
 * @param c         pointer to smb context
 * @param srv       pointer to server to add
 * @param server    server name 
 * @param share     share name
 * @param workgroup workgroup used to connect
 * @param username  username used to connect
 * @return          0 on success. 1 on failure.
 *
 */ 
static int
add_cached_server (SMBCCTX *context, SMBCSRV *new,
		   const char *server_name, const char *share_name, 
		   const char *domain, const char *username)
{
  GVfsBackendSmb *backend;

  backend = smbc_getOptionUserData (context);
  
  if (backend->cached_server != NULL)
    return 1;

  backend->cached_server_name = g_strdup (server_name);
  backend->cached_share_name = g_strdup (share_name);
  backend->cached_domain = g_strdup (domain);
  backend->cached_username = g_strdup (username);
  backend->cached_server = new;

  return 0;
}

/* Remove cached server
 *
 * @param c         pointer to smb context
 * @param srv       pointer to server to remove
 * @return          0 when found and removed. 1 on failure.
 *
 */ 
static int
remove_cached_server(SMBCCTX * context, SMBCSRV * server)
{
  GVfsBackendSmb *backend;

  backend = smbc_getOptionUserData (context);
  
  if (backend->cached_server == server)
    {
      g_free (backend->cached_server_name);
      backend->cached_server_name = NULL;
      g_free (backend->cached_share_name);
      backend->cached_share_name = NULL;
      g_free (backend->cached_domain);
      backend->cached_domain = NULL;
      g_free (backend->cached_username);
      backend->cached_username = NULL;
      backend->cached_server = NULL;
      return 0;
    }
  return 1;
}


/* Look up a server in the cache system
 *
 * @param c         pointer to smb context
 * @param server    server name to match
 * @param share     share name to match
 * @param workgroup workgroup to match
 * @param username  username to match
 * @return          pointer to SMBCSRV on success. NULL on failure.
 *
 */ 
static SMBCSRV *
get_cached_server (SMBCCTX * context,
		   const char *server_name, const char *share_name,
		   const char *domain, const char *username)
{
  GVfsBackendSmb *backend;

  backend = smbc_getOptionUserData (context);

  if (backend->cached_server != NULL &&
      strcmp (backend->cached_server_name, server_name) == 0 &&
      strcmp (backend->cached_share_name, share_name) == 0 &&
      strcmp (backend->cached_domain, domain) == 0 &&
      strcmp (backend->cached_username, username) == 0)
    return backend->cached_server;

  return NULL;
}

/* Try to remove all servers from the cache system and disconnect
 *
 * @param c         pointer to smb context
 *
 * @return          0 when found and removed. 1 on failure.
 *
 */ 
static int
purge_cached (SMBCCTX * context)
{
  GVfsBackendSmb *backend;
  
  backend = smbc_getOptionUserData (context);

  if (backend->cached_server)
    remove_cached_server(context, backend->cached_server);
  
  return 0;
}

#define SUB_DELIM_CHARS  "!$&'()*+,;="

static gboolean
is_valid (char c, const char *reserved_chars_allowed)
{
  if (g_ascii_isalnum (c) ||
      c == '-' ||
      c == '.' ||
      c == '_' ||
      c == '~')
    return TRUE;

  if (reserved_chars_allowed &&
      strchr (reserved_chars_allowed, c) != NULL)
    return TRUE;
  
  return FALSE;
}

static void
g_string_append_encoded (GString *string,
			 const char *encoded,
			 const char *reserved_chars_allowed)
{
  char c;
  static const gchar hex[16] = "0123456789ABCDEF";
  
  while ((c = *encoded++) != 0)
    {
      if (is_valid (c, reserved_chars_allowed))
	g_string_append_c (string, c);
      else
	{
	  g_string_append_c (string, '%');
	  g_string_append_c (string, hex[((guchar)c) >> 4]);
	  g_string_append_c (string, hex[((guchar)c) & 0xf]);
	}
    }
}

static GString *
create_smb_uri_string (const char *server,
		       const char *share,
		       const char *path)
{
  GString *uri;

  uri = g_string_new ("smb://");
  g_string_append_encoded (uri, server, NULL);
  g_string_append_c (uri, '/');
  g_string_append_encoded (uri, share, NULL);
  if (path != NULL)
    {
      if (*path != '/')
	g_string_append_c (uri, '/');
      g_string_append_encoded (uri, path, SUB_DELIM_CHARS ":@/");
    }

  while (uri->len > 0 &&
	 uri->str[uri->len - 1] == '/')
    g_string_erase (uri, uri->len - 1, 1);
  
  return uri;
}

static char *
create_smb_uri (const char *server,
		const char *share,
		const char *path)
{
  GString *uri;
  uri = create_smb_uri_string (server, share, path);
  return g_string_free (uri, FALSE);
}

static void
do_mount (GVfsBackend *backend,
	  GVfsJobMount *job,
	  GMountSpec *mount_spec,
	  GMountSource *mount_source,
	  gboolean is_automount)
{
  GVfsBackendSmb *op_backend = G_VFS_BACKEND_SMB (backend);
  SMBCCTX *smb_context;
  struct stat st;
  char *uri;
  int res;
  char *display_name;
  const char *debug;
  int debug_val;
  GMountSpec *smb_mount_spec;
  smbc_stat_fn smbc_stat;

  smb_context = smbc_new_context ();
  if (smb_context == NULL)
    {
      g_vfs_job_failed (G_VFS_JOB (job),
			G_IO_ERROR, G_IO_ERROR_FAILED,
			_("Internal Error (%s)"), "Failed to allocate smb context");
      return;
    }
  smbc_setOptionUserData (smb_context, backend);

  debug = g_getenv ("GVFS_SMB_DEBUG");
  if (debug)
    debug_val = atoi (debug);
  else
    debug_val = 0;

  smbc_setDebug (smb_context, debug_val);
  smbc_setFunctionAuthDataWithContext (smb_context, auth_callback);
  
  smbc_setFunctionAddCachedServer (smb_context, add_cached_server);
  smbc_setFunctionGetCachedServer (smb_context, get_cached_server);
  smbc_setFunctionRemoveCachedServer (smb_context, remove_cached_server);
  smbc_setFunctionPurgeCachedServers (smb_context, purge_cached);

  /* FIXME: is strdup() still needed here? -- removed */
  if (op_backend->default_workgroup != NULL)
    smbc_setWorkgroup (smb_context, op_backend->default_workgroup);

#ifndef DEPRECATED_SMBC_INTERFACE
  smb_context->flags = 0;
#endif
  
  /* Initial settings:
   *   - use Kerberos (always)
   *   - in case of no username specified, try anonymous login
   */
  smbc_setOptionUseKerberos (smb_context, 1);
  smbc_setOptionFallbackAfterKerberos (smb_context,
                                       op_backend->user != NULL);
  smbc_setOptionNoAutoAnonymousLogin (smb_context,
                                      op_backend->user != NULL);

  
#if 0
  smbc_setOptionDebugToStderr (smb_context, 1);
#endif
  
  if (!smbc_init_context (smb_context))
    {
      g_vfs_job_failed (G_VFS_JOB (job),
			G_IO_ERROR, G_IO_ERROR_FAILED,
			_("Internal Error (%s)"), "Failed to initialize smb context");
      smbc_free_context (smb_context, FALSE);
      return;
    }

  op_backend->smb_context = smb_context;

  /* Set the mountspec according to original uri, no matter whether user changes
     credentials during mount loop. Nautilus and other gio clients depend
     on correct mountspec, setting it to real (different) credentials would 
     lead to G_IO_ERROR_NOT_MOUNTED errors
   */

  /* Translators: This is "<sharename> on <servername>" and is used as name for an SMB share */
  display_name = g_strdup_printf (_("%s on %s"), op_backend->share, op_backend->server);
  g_vfs_backend_set_display_name (backend, display_name);
  g_free (display_name);
  g_vfs_backend_set_icon_name (backend, "folder-remote");

  smb_mount_spec = g_mount_spec_new ("smb-share");
  g_mount_spec_set (smb_mount_spec, "share", op_backend->share);
  g_mount_spec_set (smb_mount_spec, "server", op_backend->server);
  if (op_backend->user)
    g_mount_spec_set (smb_mount_spec, "user", op_backend->user);
  if (op_backend->domain)
    g_mount_spec_set (smb_mount_spec, "domain", op_backend->domain);

  g_vfs_backend_set_mount_spec (backend, smb_mount_spec);
  g_mount_spec_unref (smb_mount_spec);

  /* FIXME: we're stat()-ing user-specified path here, not the root. Ideally we
            would like to fallback to root when first mount attempt fails, though
            it would be tough to actually say if it was an authentication failure
            or the particular path problem. */
  uri = create_smb_uri (op_backend->server, op_backend->share, op_backend->path);


  /*  Samba mount loop  */
  op_backend->mount_source = mount_source;
  op_backend->mount_try = 0;
  op_backend->password_save = G_PASSWORD_SAVE_NEVER;

  do
    {
      op_backend->mount_try_again = FALSE;
      op_backend->mount_cancelled = FALSE;

      smbc_stat = smbc_getFunctionStat (smb_context);
      res = smbc_stat (smb_context, uri, &st);
      
      if (res == 0 || op_backend->mount_cancelled ||
	  (errno != EACCES && errno != EPERM))
	break;

      /* The first round is Kerberos-only.  Only if this fails do we enable
       * NTLMSSP fallback (turning off anonymous fallback, which we've
       * already tried and failed with).
       */
      if (op_backend->mount_try == 0)
        {
          smbc_setOptionFallbackAfterKerberos (op_backend->smb_context, 1);
          smbc_setOptionNoAutoAnonymousLogin (op_backend->smb_context, 1);
        }
      op_backend->mount_try ++;
    }
  while (op_backend->mount_try_again);
  
  g_free (uri);
  
  op_backend->mount_source = NULL;

  if (res != 0)
    {
      /* TODO: Error from errno? */
      op_backend->mount_source = NULL;
      
      if (op_backend->mount_cancelled) 
        g_vfs_job_failed (G_VFS_JOB (job),
			  G_IO_ERROR, G_IO_ERROR_FAILED_HANDLED,
			  _("Password dialog cancelled"));
      else
        g_vfs_job_failed (G_VFS_JOB (job),
			  G_IO_ERROR, G_IO_ERROR_FAILED,
			  /* translators: We tried to mount a windows (samba) share, but failed */
			  _("Failed to mount Windows share"));

      return;
    }

  /* Mount was successful */

  g_vfs_backend_set_default_location (backend, op_backend->path);
  g_vfs_keyring_save_password (op_backend->last_user,
			       op_backend->server,
			       op_backend->last_domain,
			       "smb",
			       NULL,
			       NULL,
			       0,
			       op_backend->last_password,
			       op_backend->password_save);
  
  g_vfs_job_succeeded (G_VFS_JOB (job));
}

static gboolean
try_mount (GVfsBackend *backend,
	   GVfsJobMount *job,
	   GMountSpec *mount_spec,
	   GMountSource *mount_source,
	   gboolean is_automount)
{
  GVfsBackendSmb *op_backend = G_VFS_BACKEND_SMB (backend);
  const char *server, *share, *user, *domain, *path;

  server = g_mount_spec_get (mount_spec, "server");
  share = g_mount_spec_get (mount_spec, "share");

  if (server == NULL || share == NULL)
    {
      g_vfs_job_failed (G_VFS_JOB (job),
			G_IO_ERROR, G_IO_ERROR_INVALID_ARGUMENT,
			_("Invalid mount spec"));
      return TRUE;
    }

  user = g_mount_spec_get (mount_spec, "user");
  domain = g_mount_spec_get (mount_spec, "domain");
  path = mount_spec->mount_prefix;
  
  op_backend->server = g_strdup (server);
  op_backend->share = g_strdup (share);
  op_backend->user = g_strdup (user);
  op_backend->domain = g_strdup (domain);
  op_backend->path = g_strdup (path);
  
  return FALSE;
}

static int
fixup_open_errno (int err)
{
  /* samba has a bug (#6228) where it doesn't set errno if path resolving failed */
  if (err == 0)
    err = ENOTDIR;
  return err;
}

static void 
do_open_for_read (GVfsBackend *backend,
		  GVfsJobOpenForRead *job,
		  const char *filename)
{
  GVfsBackendSmb *op_backend = G_VFS_BACKEND_SMB (backend);
  char *uri;
  SMBCFILE *file;
  struct stat st;
  smbc_open_fn smbc_open;
  smbc_stat_fn smbc_stat;
  int res;
  int olderr;


  uri = create_smb_uri (op_backend->server, op_backend->share, filename);
  smbc_open = smbc_getFunctionOpen (op_backend->smb_context);
  errno = 0;
  file = smbc_open (op_backend->smb_context, uri, O_RDONLY, 0);

  if (file == NULL)
    {
      olderr = fixup_open_errno (errno);
      
      smbc_stat = smbc_getFunctionStat (op_backend->smb_context);
      res = smbc_stat (op_backend->smb_context, uri, &st);
      g_free (uri);
      if ((res == 0) && (S_ISDIR (st.st_mode)))
            g_vfs_job_failed (G_VFS_JOB (job),
                              G_IO_ERROR, G_IO_ERROR_IS_DIRECTORY,
                             _("Can't open directory"));
      else
        g_vfs_job_failed_from_errno (G_VFS_JOB (job), olderr);
  }
  else
    {
      
      g_vfs_job_open_for_read_set_can_seek (job, TRUE);
      g_vfs_job_open_for_read_set_handle (job, file);
      g_vfs_job_succeeded (G_VFS_JOB (job));
    }
}

static void
do_read (GVfsBackend *backend,
	 GVfsJobRead *job,
	 GVfsBackendHandle handle,
	 char *buffer,
	 gsize bytes_requested)
{
  GVfsBackendSmb *op_backend = G_VFS_BACKEND_SMB (backend);
  ssize_t res;
  smbc_read_fn smbc_read;

  /* libsmbclient limits blocksize to (64*1024)-2 for Windows servers,
   * let's do the same here to achieve reasonable performance. (#588391)
   *
   * TODO: port to pull mechanism (#592468)
   */
  if (bytes_requested > 65534)
    bytes_requested = 65534;

  smbc_read = smbc_getFunctionRead (op_backend->smb_context);
  res = smbc_read (op_backend->smb_context, (SMBCFILE *)handle, buffer, bytes_requested);

  if (res == -1)
    g_vfs_job_failed_from_errno (G_VFS_JOB (job), errno);
  else
    {
      g_vfs_job_read_set_size (job, res);
      g_vfs_job_succeeded (G_VFS_JOB (job));
    }
}

static void
do_seek_on_read (GVfsBackend *backend,
		 GVfsJobSeekRead *job,
		 GVfsBackendHandle handle,
		 goffset    offset,
		 GSeekType  type)
{
  GVfsBackendSmb *op_backend = G_VFS_BACKEND_SMB (backend);
  int whence;
  off_t res;
  smbc_lseek_fn smbc_lseek;

  switch (type)
    {
    case G_SEEK_SET:
      whence = SEEK_SET;
      break;
    case G_SEEK_CUR:
      whence = SEEK_CUR;
      break;
    case G_SEEK_END:
      whence = SEEK_END;
      break;
    default:
      g_vfs_job_failed (G_VFS_JOB (job),
			G_IO_ERROR, G_IO_ERROR_NOT_SUPPORTED,
			_("Unsupported seek type"));
      return;
    }

  smbc_lseek = smbc_getFunctionLseek (op_backend->smb_context);
  res = smbc_lseek (op_backend->smb_context, (SMBCFILE *)handle, offset, whence);

  if (res == (off_t)-1)
    g_vfs_job_failed_from_errno (G_VFS_JOB (job), errno);
  else
    {
      g_vfs_job_seek_read_set_offset (job, res);
      g_vfs_job_succeeded (G_VFS_JOB (job));
    }

  return;
}

static void
do_query_info_on_read (GVfsBackend *backend,
		       GVfsJobQueryInfoRead *job,
		       GVfsBackendHandle handle,
		       GFileInfo *info,
		       GFileAttributeMatcher *matcher)
{
  GVfsBackendSmb *op_backend = G_VFS_BACKEND_SMB (backend);
  struct stat st = {0};
  int res, saved_errno;
  smbc_fstat_fn smbc_fstat;

  smbc_fstat = smbc_getFunctionFstat (op_backend->smb_context);
  res = smbc_fstat (op_backend->smb_context, (SMBCFILE *)handle, &st);
  saved_errno = errno;

  if (res == 0)
    {
      set_info_from_stat (op_backend, info, &st, NULL, matcher);
      
      g_vfs_job_succeeded (G_VFS_JOB (job));
    }
  else
    g_vfs_job_failed_from_errno (G_VFS_JOB (job), saved_errno);

}

static void
do_close_read (GVfsBackend *backend,
	       GVfsJobCloseRead *job,
	       GVfsBackendHandle handle)
{
  GVfsBackendSmb *op_backend = G_VFS_BACKEND_SMB (backend);
  ssize_t res;
  smbc_close_fn smbc_close;

  smbc_close = smbc_getFunctionClose (op_backend->smb_context);
  res = smbc_close (op_backend->smb_context, (SMBCFILE *)handle);
  if (res == -1)
    g_vfs_job_failed_from_errno (G_VFS_JOB (job), errno);
  else
    g_vfs_job_succeeded (G_VFS_JOB (job));
}

typedef struct {
  SMBCFILE *file;
  char *uri;
  char *tmp_uri;
  char *backup_uri;
} SmbWriteHandle;

static void
smb_write_handle_free (SmbWriteHandle *handle)
{
  g_free (handle->uri);
  g_free (handle->tmp_uri);
  g_free (handle->backup_uri);
  g_free (handle);
}

static void
do_create (GVfsBackend *backend,
	   GVfsJobOpenForWrite *job,
	   const char *filename,
	   GFileCreateFlags flags)
{
  GVfsBackendSmb *op_backend = G_VFS_BACKEND_SMB (backend);
  char *uri;
  SMBCFILE *file;
  SmbWriteHandle *handle;
  smbc_open_fn smbc_open;
  int errsv;

  uri = create_smb_uri (op_backend->server, op_backend->share, filename);
  smbc_open = smbc_getFunctionOpen (op_backend->smb_context);
  errno = 0;
  file = smbc_open (op_backend->smb_context, uri,
		    O_CREAT|O_WRONLY|O_EXCL, 0666);
  g_free (uri);

  if (file == NULL)
    {
      errsv = fixup_open_errno (errno);

      /* We guarantee EEXIST on create on existing dir */
      if (errsv == EISDIR)
	errsv = EEXIST;
      g_vfs_job_failed_from_errno (G_VFS_JOB (job), errsv);
    }
  else
    {
      handle = g_new0 (SmbWriteHandle, 1);
      handle->file = file;

      g_vfs_job_open_for_write_set_can_seek (job, TRUE);
      g_vfs_job_open_for_write_set_handle (job, handle);
      g_vfs_job_succeeded (G_VFS_JOB (job));
    }
}

static void
do_append_to (GVfsBackend *backend,
	      GVfsJobOpenForWrite *job,
	      const char *filename,
	      GFileCreateFlags flags)
{
  GVfsBackendSmb *op_backend = G_VFS_BACKEND_SMB (backend);
  char *uri;
  SMBCFILE *file;
  SmbWriteHandle *handle;
  off_t initial_offset;
  smbc_open_fn smbc_open;
  smbc_lseek_fn smbc_lseek;

  uri = create_smb_uri (op_backend->server, op_backend->share, filename);
  smbc_open = smbc_getFunctionOpen (op_backend->smb_context);
  errno = 0;
  file = smbc_open (op_backend->smb_context, uri,
					O_CREAT|O_WRONLY|O_APPEND, 0666);
  g_free (uri);

  if (file == NULL)
    g_vfs_job_failed_from_errno (G_VFS_JOB (job), fixup_open_errno (errno));
  else
    {
      handle = g_new0 (SmbWriteHandle, 1);
      handle->file = file;

      smbc_lseek = smbc_getFunctionLseek (op_backend->smb_context);
      initial_offset = smbc_lseek (op_backend->smb_context, file,
						       0, SEEK_CUR);
      if (initial_offset == (off_t) -1)
	g_vfs_job_open_for_write_set_can_seek (job, FALSE);
      else
	{
	  g_vfs_job_open_for_write_set_initial_offset (job, initial_offset);
	  g_vfs_job_open_for_write_set_can_seek (job, TRUE);
	}
      g_vfs_job_open_for_write_set_handle (job, handle);
      g_vfs_job_succeeded (G_VFS_JOB (job));
    }
}


static void
random_chars (char *str, int len)
{
  int i;
  const char chars[] = "abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789";

  for (i = 0; i < len; i++)
    str[i] = chars[g_random_int_range (0, strlen(chars))];
}

static char *
get_dir_from_uri (const char *uri)
{
  const char *prefix_end;

  prefix_end = uri + strlen (uri);

  /* Skip slashes at end */
  while (prefix_end > uri &&
	 *(prefix_end - 1) == '/')
    prefix_end--;

  /* Skip to next slash */
  while (prefix_end > uri &&
	 *(prefix_end - 1) != '/')
    prefix_end--;

  return g_strndup (uri, prefix_end - uri);
}

static SMBCFILE *
open_tmpfile (GVfsBackendSmb *backend,
	      const char *uri,
	      char **tmp_uri_out)
{
  char *dir_uri, *tmp_uri;
  char filename[] = "~gvfXXXX.tmp";
  SMBCFILE *file;
  smbc_open_fn smbc_open;

  dir_uri = get_dir_from_uri (uri);
 
  do {
    random_chars (filename + 4, 4);
    tmp_uri = g_strconcat (dir_uri, filename, NULL);

    smbc_open = smbc_getFunctionOpen (backend->smb_context);
    errno = 0;
    file = smbc_open (backend->smb_context, tmp_uri,
		      O_CREAT|O_WRONLY|O_EXCL, 0666);
  } while (file == NULL && errno == EEXIST);

  g_free (dir_uri);
  
  if (file)
    {
      *tmp_uri_out = tmp_uri;
      return file;
    }
  else
    {
      g_free (tmp_uri);
      return NULL;
    }
}

static gboolean
copy_file (GVfsBackendSmb *backend,
	   GVfsJob *job,
	   const char *from_uri,
	   const char *to_uri)
{
  SMBCFILE *from_file, *to_file;
  char buffer[4096];
  size_t buffer_size;
  ssize_t res;
  char *p;
  gboolean succeeded;
  smbc_open_fn smbc_open;
  smbc_read_fn smbc_read;
  smbc_write_fn smbc_write;
  smbc_close_fn smbc_close;
  

  from_file = NULL;
  to_file = NULL;

  succeeded = FALSE;

  smbc_open = smbc_getFunctionOpen (backend->smb_context);
  smbc_read = smbc_getFunctionRead (backend->smb_context);
  smbc_write = smbc_getFunctionWrite (backend->smb_context);
  smbc_close = smbc_getFunctionClose (backend->smb_context);

  from_file = smbc_open (backend->smb_context, from_uri,
			 O_RDONLY, 0666);
  if (from_file == NULL || g_vfs_job_is_cancelled (job))
    goto out;
  
  to_file = smbc_open (backend->smb_context, to_uri,
		       O_CREAT|O_WRONLY|O_TRUNC, 0666);
  
  if (from_file == NULL || g_vfs_job_is_cancelled (job))
    goto out;

  while (1)
    {
      
      res = smbc_read (backend->smb_context, from_file,
					buffer, sizeof(buffer));
      if (res < 0 || g_vfs_job_is_cancelled (job))
	goto out;
      if (res == 0)
	break; /* Succeeded */

      buffer_size = res;
      p = buffer;
      while (buffer_size > 0)
	{
	  res = smbc_write (backend->smb_context, to_file,
					     p, buffer_size);
	  if (res < 0 || g_vfs_job_is_cancelled (job))
	    goto out;
	  buffer_size -= res;
	  p += res;
	}
    }
  succeeded = TRUE;
 
 out: 
  if (to_file)
	  smbc_close (backend->smb_context, to_file);
  if (from_file)
	  smbc_close (backend->smb_context, from_file);
  return succeeded;
}

static char *
create_etag (struct stat *statbuf)
{
  return g_strdup_printf ("%lu", (long unsigned int)statbuf->st_mtime);
}

static void
do_replace (GVfsBackend *backend,
	    GVfsJobOpenForWrite *job,
	    const char *filename,
	    const char *etag,
	    gboolean make_backup,
	    GFileCreateFlags flags)
{
  GVfsBackendSmb *op_backend = G_VFS_BACKEND_SMB (backend);
  struct stat original_stat;
  int res;
  char *uri, *tmp_uri, *backup_uri, *current_etag;
  SMBCFILE *file;
  GError *error = NULL;
  SmbWriteHandle *handle;
  smbc_open_fn smbc_open;
  smbc_stat_fn smbc_stat;

  uri = create_smb_uri (op_backend->server, op_backend->share, filename);
  tmp_uri = NULL;
  if (make_backup)
    backup_uri = g_strconcat (uri, "~", NULL);
  else
    backup_uri = NULL;

  smbc_open = smbc_getFunctionOpen (op_backend->smb_context);
  smbc_stat = smbc_getFunctionStat (op_backend->smb_context);
  
  errno = 0;
  file = smbc_open (op_backend->smb_context, uri,
		    O_CREAT|O_WRONLY|O_EXCL, 0);
  if (file == NULL && errno != EEXIST)
    {
      int errsv = fixup_open_errno (errno);

      g_set_error_literal (&error, G_IO_ERROR,
			   g_io_error_from_errno (errsv),
			   g_strerror (errsv));
      goto error;
    }
  else if (file == NULL && errno == EEXIST)
    {
      if (etag != NULL)
	{
	  res = smbc_stat (op_backend->smb_context, uri, &original_stat);
	  
	  if (res == 0)
	    {
	      current_etag = create_etag (&original_stat);
	      if (strcmp (etag, current_etag) != 0)
		{
		  g_free (current_etag);
		  g_set_error_literal (&error,
				       G_IO_ERROR,
				       G_IO_ERROR_WRONG_ETAG,
                	               _("The file was externally modified"));
		  goto error;
		}
	      g_free (current_etag);
	    }
	}
      
      /* Backup strategy:
       *
       * By default we:
       *  1) save to a tmp file (that doesn't exist already)
       *  2) rename orig file to backup file
       *     (or delete it if no backup)
       *  3) rename tmp file to orig file
       *
       * However, this can fail if we can't write to the directory.
       * In that case we just truncate the file, after having 
       * copied directly to the backup filename.
       */

      file = open_tmpfile (op_backend, uri, &tmp_uri);
      if (file == NULL)
	{
	  if (make_backup)
	    {
	      if (!copy_file (op_backend, G_VFS_JOB (job), uri, backup_uri))
		{
		  if (g_vfs_job_is_cancelled (G_VFS_JOB (job)))
		    g_set_error_literal (&error,
					 G_IO_ERROR,
					 G_IO_ERROR_CANCELLED,
					 _("Operation was cancelled"));
		  else
		    g_set_error_literal (&error,
					 G_IO_ERROR,
					 G_IO_ERROR_CANT_CREATE_BACKUP,
					 _("Backup file creation failed"));
		  goto error;
		}
	      g_free (backup_uri);
	      backup_uri = NULL;
	    }
	  
	  errno = 0;
	  file = smbc_open (op_backend->smb_context, uri,
			    O_CREAT|O_WRONLY|O_TRUNC, 0);
	  if (file == NULL)
	    {
              int errsv = fixup_open_errno (errno);

	      g_set_error_literal (&error, G_IO_ERROR,
				   g_io_error_from_errno (errsv),
				   g_strerror (errsv));
	      goto error;
	    }
	}
    }
  else
    {
      /* Doesn't exist. Just write away */
      g_free (backup_uri);
      backup_uri = NULL;
    }

  handle = g_new (SmbWriteHandle, 1);
  handle->file = file;
  handle->uri = uri;
  handle->tmp_uri = tmp_uri;
  handle->backup_uri = backup_uri;
  
  g_vfs_job_open_for_write_set_can_seek (job, TRUE);
  g_vfs_job_open_for_write_set_handle (job, handle);
  g_vfs_job_succeeded (G_VFS_JOB (job));
  
  return;
  
 error:
  g_vfs_job_failed_from_error (G_VFS_JOB (job), error);
  g_error_free (error);
  g_free (backup_uri);
  g_free (tmp_uri);
  g_free (uri);
}


static void
do_write (GVfsBackend *backend,
	  GVfsJobWrite *job,
	  GVfsBackendHandle _handle,
	  char *buffer,
	  gsize buffer_size)
{
  GVfsBackendSmb *op_backend = G_VFS_BACKEND_SMB (backend);
  SmbWriteHandle *handle = _handle;
  ssize_t res;
  smbc_write_fn smbc_write;

  smbc_write = smbc_getFunctionWrite (op_backend->smb_context);
  res = smbc_write (op_backend->smb_context, handle->file,
					buffer, buffer_size);
  if (res == -1)
    g_vfs_job_failed_from_errno (G_VFS_JOB (job), errno);
  else
    {
      g_vfs_job_write_set_written_size (job, res);
      g_vfs_job_succeeded (G_VFS_JOB (job));
    }
}

static void
do_seek_on_write (GVfsBackend *backend,
		  GVfsJobSeekWrite *job,
		  GVfsBackendHandle _handle,
		  goffset    offset,
		  GSeekType  type)
{
  GVfsBackendSmb *op_backend = G_VFS_BACKEND_SMB (backend);
  SmbWriteHandle *handle = _handle;
  int whence;
  off_t res;
  smbc_lseek_fn smbc_lseek;

  switch (type)
    {
    case G_SEEK_SET:
      whence = SEEK_SET;
      break;
    case G_SEEK_CUR:
      whence = SEEK_CUR;
      break;
    case G_SEEK_END:
      whence = SEEK_END;
      break;
    default:
      g_vfs_job_failed (G_VFS_JOB (job),
			G_IO_ERROR, G_IO_ERROR_NOT_SUPPORTED,
			_("Unsupported seek type"));
      return;
    }

  smbc_lseek = smbc_getFunctionLseek (op_backend->smb_context);
  res = smbc_lseek (op_backend->smb_context, handle->file, offset, whence);

  if (res == (off_t)-1)
    g_vfs_job_failed_from_errno (G_VFS_JOB (job), errno);
  else
    {
      g_vfs_job_seek_write_set_offset (job, res);
      g_vfs_job_succeeded (G_VFS_JOB (job));
    }

  return;
}

static void
do_query_info_on_write (GVfsBackend *backend,
			GVfsJobQueryInfoWrite *job,
			GVfsBackendHandle _handle,
			GFileInfo *info,
			GFileAttributeMatcher *matcher)
{
  GVfsBackendSmb *op_backend = G_VFS_BACKEND_SMB (backend);
  struct stat st = {0};
  SmbWriteHandle *handle = _handle;
  int res, saved_errno;
  smbc_fstat_fn smbc_fstat;

  smbc_fstat = smbc_getFunctionFstat (op_backend->smb_context);
  res = smbc_fstat (op_backend->smb_context, handle->file, &st);
  saved_errno = errno;

  if (res == 0)
    {
      set_info_from_stat (op_backend, info, &st, NULL, matcher);
      
      g_vfs_job_succeeded (G_VFS_JOB (job));
    }
  else
    g_vfs_job_failed_from_errno (G_VFS_JOB (job), saved_errno);

}

static void
do_close_write (GVfsBackend *backend,
		GVfsJobCloseWrite *job,
		GVfsBackendHandle _handle)
{
  GVfsBackendSmb *op_backend = G_VFS_BACKEND_SMB (backend);
  SmbWriteHandle *handle = _handle;
  struct stat stat_at_close;
  int stat_res;
  ssize_t res;
  smbc_fstat_fn smbc_fstat;
  smbc_close_fn smbc_close;
  smbc_unlink_fn smbc_unlink;
  smbc_rename_fn smbc_rename;

  smbc_fstat = smbc_getFunctionFstat (op_backend->smb_context);
  smbc_close = smbc_getFunctionClose (op_backend->smb_context);
  smbc_unlink = smbc_getFunctionUnlink (op_backend->smb_context);
  smbc_rename = smbc_getFunctionRename (op_backend->smb_context);
  
  stat_res = smbc_fstat (op_backend->smb_context, handle->file, &stat_at_close);
  
  res = smbc_close (op_backend->smb_context, handle->file);

  if (res == -1)
    {
      g_vfs_job_failed_from_errno (G_VFS_JOB (job), errno);
      
      if (handle->tmp_uri)
    	  smbc_unlink (op_backend->smb_context, handle->tmp_uri);
      goto out;
    }

  if (handle->tmp_uri)
    {
      if (handle->backup_uri)
	{
	  res = smbc_rename (op_backend->smb_context, handle->uri,
						 op_backend->smb_context, handle->backup_uri);
	  if (res ==  -1)
	    {
              int errsv = errno;

          smbc_unlink (op_backend->smb_context, handle->tmp_uri);
	      g_vfs_job_failed (G_VFS_JOB (job),
				G_IO_ERROR, G_IO_ERROR_CANT_CREATE_BACKUP,
				_("Backup file creation failed: %s"), g_strerror (errsv));
	      goto out;
	    }
	}
      else
	smbc_unlink (op_backend->smb_context, handle->uri);
      
      res = smbc_rename (op_backend->smb_context, handle->tmp_uri,
					     op_backend->smb_context, handle->uri);
      if (res ==  -1)
	{
	  smbc_unlink (op_backend->smb_context, handle->tmp_uri);
	  g_vfs_job_failed_from_errno (G_VFS_JOB (job), errno);
	  goto out;
	}
    }
  
  if (stat_res == 0)
    {
      char *etag;
      etag = create_etag (&stat_at_close);
      g_vfs_job_close_write_set_etag (job, etag);
      g_free (etag);
    }
  
  g_vfs_job_succeeded (G_VFS_JOB (job));

 out:
  smb_write_handle_free (handle);  
}

static void
set_info_from_stat (GVfsBackendSmb *backend,
		    GFileInfo *info,
		    struct stat *statbuf,
		    const char *basename,
		    GFileAttributeMatcher *matcher)
{
  GFileType file_type;
  GTimeVal t;
  GIcon *icon;
  char *content_type;
  char *display_name;

  if (basename)
    {
      g_file_info_set_name (info, basename);
      if (*basename == '.')
	g_file_info_set_is_hidden (info, TRUE);
    }

  
  if (basename != NULL &&
      g_file_attribute_matcher_matches (matcher,
                                        G_FILE_ATTRIBUTE_STANDARD_DISPLAY_NAME))
    {
      if (strcmp (basename, "/") == 0)
	display_name = g_strdup_printf (_("%s on %s"), backend->share, backend->server);
      else
	display_name = g_filename_display_name (basename);
      
      if (strstr (display_name, "\357\277\275") != NULL)
        {
          char *p = display_name;
          display_name = g_strconcat (display_name, _(" (invalid encoding)"), NULL);
          g_free (p);
        }
      g_file_info_set_display_name (info, display_name);
      g_free (display_name);
    }
  
  if (basename != NULL &&
      g_file_attribute_matcher_matches (matcher,
                                        G_FILE_ATTRIBUTE_STANDARD_EDIT_NAME))
    {
      char *edit_name = g_filename_display_name (basename);
      g_file_info_set_edit_name (info, edit_name);
      g_free (edit_name);
    }

  
  file_type = G_FILE_TYPE_UNKNOWN;

  if (S_ISREG (statbuf->st_mode))
    file_type = G_FILE_TYPE_REGULAR;
  else if (S_ISDIR (statbuf->st_mode))
    file_type = G_FILE_TYPE_DIRECTORY;
  else if (S_ISCHR (statbuf->st_mode) ||
	   S_ISBLK (statbuf->st_mode) ||
	   S_ISFIFO (statbuf->st_mode)
#ifdef S_ISSOCK
	   || S_ISSOCK (statbuf->st_mode)
#endif
	   )
    file_type = G_FILE_TYPE_SPECIAL;
  else if (S_ISLNK (statbuf->st_mode))
    file_type = G_FILE_TYPE_SYMBOLIC_LINK;

  g_file_info_set_file_type (info, file_type);
  g_file_info_set_size (info, statbuf->st_size);

  t.tv_sec = statbuf->st_mtime;
#if defined (HAVE_STRUCT_STAT_ST_MTIMENSEC)
  t.tv_usec = statbuf->st_mtimensec / 1000;
#elif defined (HAVE_STRUCT_STAT_ST_MTIM_TV_NSEC)
  t.tv_usec = statbuf->st_mtim.tv_nsec / 1000;
#else
  t.tv_usec = 0;
#endif
  g_file_info_set_modification_time (info, &t);


  if (g_file_attribute_matcher_matches (matcher,
					G_FILE_ATTRIBUTE_STANDARD_CONTENT_TYPE) ||
      g_file_attribute_matcher_matches (matcher,
					G_FILE_ATTRIBUTE_STANDARD_ICON))
    {
      icon = NULL;
      content_type = NULL;
      
      if (S_ISDIR(statbuf->st_mode))
	{
	  content_type = g_strdup ("inode/directory");
	  if (basename != NULL && strcmp (basename, "/") == 0)
	    icon = g_themed_icon_new ("folder-remote");
	  else
	    icon = g_themed_icon_new ("folder");
	}
      else if (basename != NULL)
	{
	  content_type = g_content_type_guess (basename, NULL, 0, NULL);
	  if (content_type)
	    icon = g_content_type_get_icon (content_type);
	}
      
      if (content_type)
	{
	  g_file_info_set_content_type (info, content_type);
	  g_free (content_type);
	}

      if (icon == NULL)
	icon = g_themed_icon_new ("text-x-generic");
      
      g_file_info_set_icon (info, icon);
      g_object_unref (icon);
  }
  
  /* Don't trust n_link, uid, gid, etc returned from libsmb, its just made up.
     These are ok though: */

  g_file_info_set_attribute_uint32 (info, G_FILE_ATTRIBUTE_UNIX_DEVICE, statbuf->st_dev);
  g_file_info_set_attribute_uint64 (info, G_FILE_ATTRIBUTE_UNIX_INODE, statbuf->st_ino);

  /* If file is dos-readonly, libsmbclient doesn't set S_IWUSR, we use this to
     trigger ACCESS_WRITE = FALSE: */
  if (!(statbuf->st_mode & S_IWUSR))
    g_file_info_set_attribute_boolean (info, G_FILE_ATTRIBUTE_ACCESS_CAN_WRITE, FALSE);

  g_file_info_set_attribute_uint64 (info, G_FILE_ATTRIBUTE_TIME_ACCESS, statbuf->st_atime);
#if defined (HAVE_STRUCT_STAT_ST_ATIMENSEC)
  g_file_info_set_attribute_uint32 (info, G_FILE_ATTRIBUTE_TIME_ACCESS_USEC, statbuf->st_atimensec / 1000);
#elif defined (HAVE_STRUCT_STAT_ST_ATIM_TV_NSEC)
  g_file_info_set_attribute_uint32 (info, G_FILE_ATTRIBUTE_TIME_ACCESS_USEC, statbuf->st_atim.tv_nsec / 1000);
#endif
  g_file_info_set_attribute_uint64 (info, G_FILE_ATTRIBUTE_TIME_CHANGED, statbuf->st_ctime);
#if defined (HAVE_STRUCT_STAT_ST_CTIMENSEC)
  g_file_info_set_attribute_uint32 (info, G_FILE_ATTRIBUTE_TIME_CHANGED_USEC, statbuf->st_ctimensec / 1000);
#elif defined (HAVE_STRUCT_STAT_ST_CTIM_TV_NSEC)
  g_file_info_set_attribute_uint32 (info, G_FILE_ATTRIBUTE_TIME_CHANGED_USEC, statbuf->st_ctim.tv_nsec / 1000);
#endif

  /* Libsmb sets the X bit on files to indicate some special things: */
  if ((statbuf->st_mode & S_IFDIR) == 0) {
    
    if (statbuf->st_mode & S_IXOTH)
      g_file_info_set_is_hidden (info, TRUE);
    
    if (statbuf->st_mode & S_IXUSR)
      g_file_info_set_attribute_boolean (info, G_FILE_ATTRIBUTE_DOS_IS_ARCHIVE, TRUE);
    
    if (statbuf->st_mode & S_IXGRP)
      g_file_info_set_attribute_boolean (info, G_FILE_ATTRIBUTE_DOS_IS_SYSTEM, TRUE);
  }

  if (g_file_attribute_matcher_matches (matcher, G_FILE_ATTRIBUTE_ETAG_VALUE))
    {
      char *etag = create_etag (statbuf);
      g_file_info_set_attribute_string (info, G_FILE_ATTRIBUTE_ETAG_VALUE, etag);
      g_free (etag);
    }
}

static void
do_query_info (GVfsBackend *backend,
	       GVfsJobQueryInfo *job,
	       const char *filename,
	       GFileQueryInfoFlags flags,
	       GFileInfo *info,
	       GFileAttributeMatcher *matcher)
{
  GVfsBackendSmb *op_backend = G_VFS_BACKEND_SMB (backend);
  struct stat st = {0};
  char *uri;
  int res, saved_errno;
  char *basename;
  smbc_stat_fn smbc_stat;

  uri = create_smb_uri (op_backend->server, op_backend->share, filename);
  smbc_stat = smbc_getFunctionStat (op_backend->smb_context);
  res = smbc_stat (op_backend->smb_context, uri, &st);
  saved_errno = errno;
  g_free (uri);

  if (res == 0)
    {
      basename = g_path_get_basename (filename);
      set_info_from_stat (op_backend, info, &st, basename, matcher);
      g_free (basename);
      
      g_vfs_job_succeeded (G_VFS_JOB (job));
    }
  else
    g_vfs_job_failed_from_errno (G_VFS_JOB (job), saved_errno);

}

static void
do_query_fs_info (GVfsBackend *backend,
		  GVfsJobQueryFsInfo *job,
		  const char *filename,
		  GFileInfo *info,
		  GFileAttributeMatcher *attribute_matcher)
{
  GVfsBackendSmb *op_backend = G_VFS_BACKEND_SMB (backend);

  g_file_info_set_attribute_string (info, G_FILE_ATTRIBUTE_FILESYSTEM_TYPE, "cifs");

#ifdef HAVE_SAMBA_STAT_VFS
  smbc_statvfs_fn smbc_statvfs;
  struct statvfs st = {0};
  char *uri;
  int res;

  if (g_file_attribute_matcher_matches (attribute_matcher,
					G_FILE_ATTRIBUTE_FILESYSTEM_SIZE) ||
      g_file_attribute_matcher_matches (attribute_matcher,
					G_FILE_ATTRIBUTE_FILESYSTEM_FREE) ||
      g_file_attribute_matcher_matches (attribute_matcher,
					G_FILE_ATTRIBUTE_FILESYSTEM_READONLY))
    {
      uri = create_smb_uri (op_backend->server, op_backend->share, filename);
      smbc_statvfs = smbc_getFunctionStatVFS (op_backend->smb_context);
      res = smbc_statvfs (op_backend->smb_context, uri, &st);
      g_free (uri);

      if (res == 0)
        {
          /* older samba versions ( < 3.0.28) return zero values in struct statvfs */
          if (st.f_blocks > 0)
            {
              /* FIXME: inconsistent return values (libsmbclient-3.4.2)
               *       - for linux samba hosts, f_frsize is zero and f_bsize is a real block size
               *       - for some Windows hosts (XP), f_frsize and f_bsize should be multiplied to get real block size
               */
              g_file_info_set_attribute_uint64 (info, G_FILE_ATTRIBUTE_FILESYSTEM_SIZE, st.f_bsize * st.f_blocks * ((st.f_frsize == 0) ? 1 : st.f_frsize));
              g_file_info_set_attribute_uint64 (info, G_FILE_ATTRIBUTE_FILESYSTEM_FREE, st.f_bsize * st.f_bfree * ((st.f_frsize == 0) ? 1 : st.f_frsize));
              g_file_info_set_attribute_boolean (info, G_FILE_ATTRIBUTE_FILESYSTEM_READONLY, st.f_flag & SMBC_VFS_FEATURE_RDONLY);
            }
        }
    }
#endif

  g_vfs_job_succeeded (G_VFS_JOB (job));
}


static gboolean
try_query_settable_attributes (GVfsBackend *backend,
			       GVfsJobQueryAttributes *job,
			       const char *filename)
{
  GFileAttributeInfoList *list;

  list = g_file_attribute_info_list_new ();

  /* TODO: Add all settable attributes here -- bug #559586 */
  /* TODO: xattrs support? */

  g_file_attribute_info_list_add (list,
                                  G_FILE_ATTRIBUTE_TIME_MODIFIED,
                                  G_FILE_ATTRIBUTE_TYPE_UINT64,
                                  G_FILE_ATTRIBUTE_INFO_COPY_WITH_FILE |
                                  G_FILE_ATTRIBUTE_INFO_COPY_WHEN_MOVED);

#if 0
/* FIXME: disabled; despite chmod is supported, it makes no sense on samba shares and
          libsmbclient lacks proper API to read unix file modes.
          The struct stat->st_mode member is used for special Windows attributes. */
  g_file_attribute_info_list_add (list,
                                  G_FILE_ATTRIBUTE_UNIX_MODE,
                                  G_FILE_ATTRIBUTE_TYPE_UINT32,
                                  G_FILE_ATTRIBUTE_INFO_COPY_WITH_FILE |
                                  G_FILE_ATTRIBUTE_INFO_COPY_WHEN_MOVED);
#endif

  g_vfs_job_query_attributes_set_list (job, list);
  g_vfs_job_succeeded (G_VFS_JOB (job));
  
  g_file_attribute_info_list_unref (list);
  return TRUE;
}

static void
do_set_attribute (GVfsBackend *backend,
                  GVfsJobSetAttribute *job,
                  const char *filename,
                  const char *attribute,
                  GFileAttributeType type,
                  gpointer value_p,
                  GFileQueryInfoFlags flags)
{
  GVfsBackendSmb *op_backend;
  char *uri;
  int res, errsv;
  struct timeval tbuf[2];
  smbc_utimes_fn smbc_utimes;
#if 0
  smbc_chmod_fn smbc_chmod;
#endif


  op_backend = G_VFS_BACKEND_SMB (backend);

  if (strcmp (attribute, G_FILE_ATTRIBUTE_TIME_MODIFIED) != 0
#if 0
      && strcmp (attribute, G_FILE_ATTRIBUTE_UNIX_MODE) != 0
#endif
      )
    {
      g_vfs_job_failed (G_VFS_JOB (job),
                        G_IO_ERROR, G_IO_ERROR_NOT_SUPPORTED,
                        _("Operation unsupported"));
      return;
    }

  uri = create_smb_uri (op_backend->server, op_backend->share, filename);
  res = -1;

  if (strcmp (attribute, G_FILE_ATTRIBUTE_TIME_MODIFIED) == 0)
    {
      if (type != G_FILE_ATTRIBUTE_TYPE_UINT64) 
        {
          g_vfs_job_failed (G_VFS_JOB (job),
                            G_IO_ERROR,
                            G_IO_ERROR_INVALID_ARGUMENT,
                            "%s",
                            _("Invalid attribute type (uint64 expected)"));
        }

      smbc_utimes = smbc_getFunctionUtimes (op_backend->smb_context);
      tbuf[1].tv_sec = (*(guint64 *)value_p);  /* mtime */
      tbuf[1].tv_usec = 0;
      /* atime = mtime (atimes are usually disabled on desktop systems) */
      tbuf[0].tv_sec = tbuf[1].tv_sec;  
      tbuf[0].tv_usec = 0;
      res = smbc_utimes (op_backend->smb_context, uri, &tbuf[0]);
    }
#if 0
  else
  if (strcmp (attribute, G_FILE_ATTRIBUTE_UNIX_MODE) == 0)
    {
      smbc_chmod = smbc_getFunctionChmod (op_backend->smb_context);
      res = smbc_chmod (op_backend->smb_context, uri, (*(guint32 *)value_p) & 0777);
    }
#endif    

  errsv = errno;
  g_free (uri);

  if (res != 0)
    g_vfs_job_failed_from_errno (G_VFS_JOB (job), errsv);
  else
    g_vfs_job_succeeded (G_VFS_JOB (job));
}

static void
do_enumerate (GVfsBackend *backend,
	      GVfsJobEnumerate *job,
	      const char *filename,
	      GFileAttributeMatcher *matcher,
	      GFileQueryInfoFlags flags)
{
  GVfsBackendSmb *op_backend = G_VFS_BACKEND_SMB (backend);
  struct stat st;
  int res;
  GError *error;
  SMBCFILE *dir;
  char dirents[1024*4];
  struct smbc_dirent *dirp;
  GList *files;
  GFileInfo *info;
  GString *uri;
  int uri_start_len;
  smbc_opendir_fn smbc_opendir;
  smbc_getdents_fn smbc_getdents;
  smbc_stat_fn smbc_stat;
  smbc_closedir_fn smbc_closedir;

  uri = create_smb_uri_string (op_backend->server, op_backend->share, filename);
  
  smbc_opendir = smbc_getFunctionOpendir (op_backend->smb_context);
  smbc_getdents = smbc_getFunctionGetdents (op_backend->smb_context);
  smbc_stat = smbc_getFunctionStat (op_backend->smb_context);
  smbc_closedir = smbc_getFunctionClosedir (op_backend->smb_context);
  
  dir = smbc_opendir (op_backend->smb_context, uri->str);

  if (dir == NULL)
    {
      int errsv = errno;

      error = NULL;
      g_set_error_literal (&error, G_IO_ERROR,
			   g_io_error_from_errno (errsv),
			   g_strerror (errsv));
      goto error;
    }

  g_vfs_job_succeeded (G_VFS_JOB (job));

  if (uri->str[uri->len - 1] != '/')
    g_string_append_c (uri, '/');
  uri_start_len = uri->len;

  while (TRUE)
    {
      files = NULL;
      
      res = smbc_getdents (op_backend->smb_context, dir, (struct smbc_dirent *)dirents, sizeof (dirents));
      if (res <= 0)
	break;
      
      dirp = (struct smbc_dirent *)dirents;
      while (res > 0)
	{
	  unsigned int dirlen;

	  /* TODO: Only do stat if required for flags */
	  
	  if ((dirp->smbc_type == SMBC_DIR ||
	       dirp->smbc_type == SMBC_FILE ||
	       dirp->smbc_type == SMBC_LINK) &&
	      strcmp (dirp->name, ".") != 0 &&
	      strcmp (dirp->name, "..") != 0)
	    {
	      int stat_res;
	      g_string_truncate (uri, uri_start_len);
	      g_string_append_encoded (uri,
				       dirp->name,
				       SUB_DELIM_CHARS ":@/");

	      if (matcher == NULL ||
		  g_file_attribute_matcher_matches_only (matcher, G_FILE_ATTRIBUTE_STANDARD_NAME))
		{
		  info = g_file_info_new ();
		  g_file_info_set_name (info, dirp->name);
		  files = g_list_prepend (files, info);
		}
	      else
		{
		  stat_res = smbc_stat (op_backend->smb_context,
							    uri->str, &st);
		  if (stat_res == 0)
		    {
		      info = g_file_info_new ();
		      set_info_from_stat (op_backend, info, &st, dirp->name, matcher);
		      files = g_list_prepend (files, info);
		    }
		}
	    }
	  
	  dirlen = dirp->dirlen;
	  dirp = (struct smbc_dirent *) (((char *)dirp) + dirlen);
	  res -= dirlen;
	}
      
      if (files)
	{
	  files = g_list_reverse (files);
	  g_vfs_job_enumerate_add_infos (job, files);
	  g_list_foreach (files, (GFunc)g_object_unref, NULL);
	  g_list_free (files);
	}
    }
      
  res = smbc_closedir (op_backend->smb_context, dir);

  g_vfs_job_enumerate_done (job);

  g_string_free (uri, TRUE);
  return;
  
 error:
  g_vfs_job_failed_from_error (G_VFS_JOB (job), error);
  g_error_free (error);
  g_string_free (uri, TRUE);
}

static void
do_set_display_name (GVfsBackend *backend,
		     GVfsJobSetDisplayName *job,
		     const char *filename,
		     const char *display_name)
{
  GVfsBackendSmb *op_backend = G_VFS_BACKEND_SMB (backend);
  char *from_uri, *to_uri;
  char *dirname, *new_path;
  int res, errsv;
  struct stat st;
  smbc_rename_fn smbc_rename;
  smbc_stat_fn smbc_stat;

  dirname = g_path_get_dirname (filename);

  /* TODO: display name is in utf8, atm we assume libsmb uris
     are in utf8, but this might not be true if the user changed
     the smb.conf file. Can we check this and convert? */

  new_path = g_build_filename (dirname, display_name, NULL);
  g_free (dirname);
  
  from_uri = create_smb_uri (op_backend->server, op_backend->share, filename);
  to_uri = create_smb_uri (op_backend->server, op_backend->share, new_path);
  

  /* We can't rely on libsmbclient reporting EEXIST, let's always stat first.
   * https://bugzilla.gnome.org/show_bug.cgi?id=616645
   */
  smbc_stat = smbc_getFunctionStat (op_backend->smb_context);
  res = smbc_stat (op_backend->smb_context, to_uri, &st);
  if (res == 0)
    {
      g_vfs_job_failed (G_VFS_JOB (job),
                        G_IO_ERROR, G_IO_ERROR_EXISTS,
                        _("Can't rename file, filename already exists"));
      goto out;
    }

  smbc_rename = smbc_getFunctionRename (op_backend->smb_context);
  res = smbc_rename (op_backend->smb_context, from_uri,
                     op_backend->smb_context, to_uri);
  errsv = errno;

  if (res != 0)
    g_vfs_job_failed_from_errno (G_VFS_JOB (job), errsv);
  else
    {
      g_vfs_job_set_display_name_set_new_path (job, new_path);
      g_vfs_job_succeeded (G_VFS_JOB (job));
    }

 out:
  g_free (from_uri);
  g_free (to_uri);
  g_free (new_path);
}

static void
do_delete (GVfsBackend *backend,
	   GVfsJobDelete *job,
	   const char *filename)
{
  GVfsBackendSmb *op_backend = G_VFS_BACKEND_SMB (backend);
  struct stat statbuf;
  char *uri;
  int errsv, res;
  smbc_stat_fn smbc_stat;
  smbc_rmdir_fn smbc_rmdir;
  smbc_unlink_fn smbc_unlink;


  uri = create_smb_uri (op_backend->server, op_backend->share, filename);

  smbc_stat = smbc_getFunctionStat (op_backend->smb_context);
  smbc_rmdir = smbc_getFunctionRmdir (op_backend->smb_context);
  smbc_unlink = smbc_getFunctionUnlink (op_backend->smb_context);

  res = smbc_stat (op_backend->smb_context, uri, &statbuf);
  if (res == -1)
    {
      errsv = errno;

      g_vfs_job_failed (G_VFS_JOB (job),
			G_IO_ERROR,
			g_io_error_from_errno (errsv),
			_("Error deleting file: %s"),
			g_strerror (errsv));
      g_free (uri);
      return;
    }

  if (S_ISDIR (statbuf.st_mode))
    res = smbc_rmdir (op_backend->smb_context, uri);
  else
    res = smbc_unlink (op_backend->smb_context, uri);
  errsv = errno;
  g_free (uri);

  if (res != 0)
    g_vfs_job_failed_from_errno (G_VFS_JOB (job), errsv);
  else
    g_vfs_job_succeeded (G_VFS_JOB (job));
}

static void
do_make_directory (GVfsBackend *backend,
		   GVfsJobMakeDirectory *job,
		   const char *filename)
{
  GVfsBackendSmb *op_backend = G_VFS_BACKEND_SMB (backend);
  char *uri;
  int errsv, res;
  smbc_mkdir_fn smbc_mkdir;

  uri = create_smb_uri (op_backend->server, op_backend->share, filename);
  smbc_mkdir = smbc_getFunctionMkdir (op_backend->smb_context);
  res = smbc_mkdir (op_backend->smb_context, uri, 0666);
  errsv = errno;
  g_free (uri);

  if (res != 0)
    g_vfs_job_failed_from_errno (G_VFS_JOB (job), errsv);
  else
    g_vfs_job_succeeded (G_VFS_JOB (job));
}

static void
do_move (GVfsBackend *backend,
	 GVfsJobMove *job,
	 const char *source,
	 const char *destination,
	 GFileCopyFlags flags,
	 GFileProgressCallback progress_callback,
	 gpointer progress_callback_data)
{
  GVfsBackendSmb *op_backend = G_VFS_BACKEND_SMB (backend);
  char *source_uri, *dest_uri, *backup_uri;
  gboolean destination_exist, source_is_dir;
  struct stat statbuf;
  int res, errsv;
  smbc_stat_fn smbc_stat;
  smbc_rename_fn smbc_rename;
  smbc_unlink_fn smbc_unlink;

  
  source_uri = create_smb_uri (op_backend->server, op_backend->share, source);

  smbc_stat = smbc_getFunctionStat (op_backend->smb_context);
  smbc_rename = smbc_getFunctionRename (op_backend->smb_context);
  smbc_unlink = smbc_getFunctionUnlink (op_backend->smb_context);

  res = smbc_stat (op_backend->smb_context, source_uri, &statbuf);
  if (res == -1)
    {
      errsv = errno;

      g_vfs_job_failed (G_VFS_JOB (job),
			G_IO_ERROR,
			g_io_error_from_errno (errsv),
			_("Error moving file: %s"),
			g_strerror (errsv));
      g_free (source_uri);
      return;
    }
  else
    source_is_dir = S_ISDIR (statbuf.st_mode);

  dest_uri = create_smb_uri (op_backend->server, op_backend->share, destination);
  
  destination_exist = FALSE;
  res = smbc_stat (op_backend->smb_context, dest_uri, &statbuf);
  if (res == 0)
    {
      destination_exist = TRUE; /* Target file exists */

      if (flags & G_FILE_COPY_OVERWRITE)
	{
	  /* Always fail on dirs, even with overwrite */
	  if (S_ISDIR (statbuf.st_mode))
	    {
	      g_vfs_job_failed (G_VFS_JOB (job),
				G_IO_ERROR,
				G_IO_ERROR_WOULD_MERGE,
				_("Can't move directory over directory"));
	      g_free (source_uri);
	      g_free (dest_uri);
	      return;
	    }
	}
      else
	{
	  g_vfs_job_failed (G_VFS_JOB (job),
			    G_IO_ERROR,
			    G_IO_ERROR_EXISTS,
			    _("Target file already exists"));
	  g_free (source_uri);
	  g_free (dest_uri);
	  return;
	}
    }

  if (flags & G_FILE_COPY_BACKUP && destination_exist)
    {
      backup_uri = g_strconcat (dest_uri, "~", NULL);
      res = smbc_rename (op_backend->smb_context, dest_uri,
					     op_backend->smb_context, backup_uri);
      if (res == -1)
	{
	  g_vfs_job_failed (G_VFS_JOB (job),
			    G_IO_ERROR,
			    G_IO_ERROR_CANT_CREATE_BACKUP,
			    _("Backup file creation failed"));
	  g_free (source_uri);
	  g_free (dest_uri);
	  g_free (backup_uri);
	  return;
	}
      g_free (backup_uri);
      destination_exist = FALSE; /* It did, but no more */
    }

  if (source_is_dir && destination_exist && (flags & G_FILE_COPY_OVERWRITE))
    {
      /* Source is a dir, destination exists (and is not a dir, because that would have failed
	 earlier), and we're overwriting. Manually remove the target so we can do the rename. */
      res = smbc_unlink (op_backend->smb_context, dest_uri);
      errsv = errno;
      if (res == -1)
	{
	  g_vfs_job_failed (G_VFS_JOB (job), G_IO_ERROR,
			    g_io_error_from_errno (errsv),
			    _("Error removing target file: %s"),
			    g_strerror (errsv));
	  g_free (source_uri);
	  g_free (dest_uri);
	  return;
	}
    }

  
  res = smbc_rename (op_backend->smb_context, source_uri,
					 op_backend->smb_context, dest_uri);
  errsv = errno;
  g_free (source_uri);
  g_free (dest_uri);

  /* Catch moves across device boundaries */
  if (res != 0)
    {
      if (errsv == EXDEV ||
	  /* Unfortunately libsmbclient doesn't correctly return EXDEV, but falls back
	     to EINVAL, so we try to guess when this happens: */
	  (errsv == EINVAL && source_is_dir))
	g_vfs_job_failed (G_VFS_JOB (job), 
			  G_IO_ERROR, G_IO_ERROR_WOULD_RECURSE,
			  _("Can't recursively move directory"));
      else
	g_vfs_job_failed_from_errno (G_VFS_JOB (job), errsv);
    }
  else
    g_vfs_job_succeeded (G_VFS_JOB (job));
}

static void
g_vfs_backend_smb_class_init (GVfsBackendSmbClass *klass)
{
  GObjectClass *gobject_class = G_OBJECT_CLASS (klass);
  GVfsBackendClass *backend_class = G_VFS_BACKEND_CLASS (klass);
  
  gobject_class->finalize = g_vfs_backend_smb_finalize;

  backend_class->mount = do_mount;
  backend_class->try_mount = try_mount;
  backend_class->open_for_read = do_open_for_read;
  backend_class->read = do_read;
  backend_class->seek_on_read = do_seek_on_read;
  backend_class->query_info_on_read = do_query_info_on_read;
  backend_class->close_read = do_close_read;
  backend_class->create = do_create;
  backend_class->append_to = do_append_to;
  backend_class->replace = do_replace;
  backend_class->write = do_write;
  backend_class->seek_on_write = do_seek_on_write;
  backend_class->query_info_on_write = do_query_info_on_write;
  backend_class->close_write = do_close_write;
  backend_class->query_info = do_query_info;
  backend_class->query_fs_info = do_query_fs_info;
  backend_class->enumerate = do_enumerate;
  backend_class->set_display_name = do_set_display_name;
  backend_class->delete = do_delete;
  backend_class->make_directory = do_make_directory;
  backend_class->move = do_move;
  backend_class->try_query_settable_attributes = try_query_settable_attributes;
  backend_class->set_attribute = do_set_attribute;
}

void
g_vfs_smb_daemon_init (void)
{
  g_set_application_name (_("Windows Shares Filesystem Service"));
}
