/*
* gnome-keyring
*
* Copyright (C) 2008 Stefan Walter
*
* This program 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.1 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
* Lesser General Public License for more details.
*
* You should have received a copy of the GNU Lesser General Public
* License along with this program; if not, see
* .
*/
#include "config.h"
#include "gkm-marshal.h"
#include "gkm-transaction.h"
#include
#include
#include
#include
#include
#ifndef O_BINARY
# define O_BINARY 0
#endif
enum {
PROP_0,
PROP_COMPLETED,
PROP_FAILED,
PROP_RESULT
};
enum {
COMPLETE,
LAST_SIGNAL
};
static guint signals[LAST_SIGNAL] = { 0 };
struct _GkmTransaction {
GObject parent;
GList *completes;
gboolean failed;
gboolean completed;
CK_RV result;
};
typedef struct _Complete {
GObject *object;
GkmTransactionFunc func;
gpointer user_data;
} Complete;
G_DEFINE_TYPE (GkmTransaction, gkm_transaction, G_TYPE_OBJECT);
#define MAX_TRIES 100000
/* -----------------------------------------------------------------------------
* INTERNAL
*/
static gboolean
complete_invoke (GkmTransaction *transaction, Complete *complete)
{
g_assert (complete);
g_assert (complete->func);
return (complete->func) (transaction, complete->object, complete->user_data);
}
static void
complete_destroy (Complete *complete)
{
g_assert (complete->func);
if (complete->object)
g_object_unref (complete->object);
g_slice_free (Complete, complete);
}
static gboolean
complete_accumulator (GSignalInvocationHint *ihint, GValue *return_accu,
const GValue *handler_return, gpointer data)
{
gboolean result;
/* If any of them return false, then the result is false */
result = g_value_get_boolean (handler_return);
if (result == FALSE)
g_value_set_boolean (return_accu, FALSE);
/* Continue signal invocations */
return TRUE;
}
static gboolean
complete_new_file (GkmTransaction *self, GObject *unused, gpointer user_data)
{
gchar *path = user_data;
gboolean ret = TRUE;
if (gkm_transaction_get_failed (self)) {
if (g_unlink (path) < 0) {
g_warning ("couldn't delete aborted file, data may be lost: %s: %s",
path, g_strerror (errno));
ret = FALSE;
}
}
g_free (path);
return ret;
}
static gboolean
begin_new_file (GkmTransaction *self, const gchar *filename)
{
g_assert (GKM_IS_TRANSACTION (self));
g_assert (!gkm_transaction_get_failed (self));
g_assert (filename);
gkm_transaction_add (self, NULL, complete_new_file, g_strdup (filename));
return TRUE;
}
static gboolean
complete_link_temporary (GkmTransaction *self, GObject *unused, gpointer user_data)
{
gchar *path = user_data;
gboolean ret = TRUE;
gchar *original;
gchar *ext;
/* When failed, rename temporary back */
if (gkm_transaction_get_failed (self)) {
/* Figure out the original file name */
original = g_strdup (path);
ext = strrchr (original, '.');
g_return_val_if_fail (ext, FALSE);
*ext = '\0';
/* Now rename us back */
if (g_rename (path, original) == -1) {
g_warning ("couldn't restore original file, data may be lost: %s: %s",
original, g_strerror (errno));
ret = FALSE;
}
g_free (original);
/* When succeeded, remove temporary */
} else {
if (g_unlink (path) == -1) {
g_warning ("couldn't delete temporary backup file: %s: %s",
path, g_strerror (errno));
ret = TRUE; /* Not actually that bad of a situation */
}
}
g_free (path);
return ret;
}
/* Copy the file SRCNAME to the file DSTNAME. If DSTNAME already
exists -1 is returned and ERRNO set to EEXIST. Returns 0 on
success. */
static int
copy_to_temp_file (const char *dstname, const char *srcname)
{
int dstfd, srcfd;
int nread, nwritten;
int saveerr;
char *bufp;
char buffer[512]; /* If you change this size, please also adjust */
/* test-transaction.c:test_write_large_file. */
do {
srcfd = g_open (srcname, (O_RDONLY | O_BINARY));
} while (srcfd == -1 && errno == EINTR);
if (srcfd == -1) {
saveerr = errno;
g_warning ("couldn't open file to make temporary copy from: %s: %s",
srcname, g_strerror (saveerr));
errno = saveerr;
return -1;
}
do {
dstfd = g_open (dstname,
(O_WRONLY | O_CREAT | O_EXCL | O_BINARY),
(S_IRUSR | S_IWUSR));
} while (dstfd == -1 && errno == EINTR);
if (dstfd == -1) {
saveerr = errno;
close (srcfd);
errno = saveerr;
return -1;
}
while ((nread = read (srcfd, buffer, sizeof buffer))) {
if (nread == -1 && errno == EINTR)
continue;
if (nread == -1) {
saveerr = errno;
g_warning ("error reading file to make temporary copy from: %s: %s",
srcname, g_strerror (saveerr));
goto failure;
}
bufp = buffer;
do {
do {
nwritten = write (dstfd, bufp, nread);
} while (nwritten == -1 && errno == EINTR);
if (nwritten == -1) {
saveerr = errno;
g_warning ("error wrinting to temporary file: %s: %s",
dstname, g_strerror (saveerr));
goto failure;
}
g_return_val_if_fail (nwritten <= nread, -1);
nread -= nwritten;
bufp += nwritten;
} while (nread > 0);
}
/* EOF reached. */
if (close (dstfd)) {
saveerr = errno;
g_warning ("error closing temporary file: %s: %s",
dstname, g_strerror (saveerr));
goto failure;
}
close (srcfd);
return 0;
failure:
close (dstfd); /* (Doesn't harm if we try a second time.) */
if (g_unlink (dstname))
g_warning ("couldn't remove temporary file: %s: %s",
dstname, g_strerror (saveerr));
close (srcfd);
errno = saveerr;
return -1;
}
static gboolean
begin_link_temporary_if_exists (GkmTransaction *self, const gchar *filename, gboolean *exists)
{
guint i = 0;
g_assert (GKM_IS_TRANSACTION (self));
g_assert (!gkm_transaction_get_failed (self));
g_assert (filename);
g_assert (exists);
for (i = 0; i < MAX_TRIES; ++i) {
struct stat sb;
unsigned int nlink;
int stat_failed = 0;
*exists = TRUE;
/* Try to link to random temporary file names. We try
* to use a hardlink to create a copy but if that
* fails (i.e. not supported by the FS), we copy the
* entire file. The result should be the same except
* that the file times will change if we need to
* rollback the transaction. */
if (stat (filename, &sb)) {
stat_failed = 1;
} else {
gchar *result;
result = g_strdup_printf ("%s.temp-%d", filename,
g_random_int_range (0, G_MAXINT));
nlink = (unsigned int)sb.st_nlink;
/* The result code of link(2) is not reliable.
* Unless it fails with EEXIST we stat the
* file to check for success. Note that there
* is a race here: If another process adds a
* link to the source file between link and
* stat, the check on the increased link count
* will fail. Fortunately the case for
* hardlinks are not working solves it. */
if (link (filename, result) && errno == EEXIST) {
/* This is probably a valid error.
* Let us try another temporary file. */
} else if (stat (filename, &sb)) {
stat_failed = 1;
} else {
if ((sb.st_nlink == nlink + 1)
|| !copy_to_temp_file (result, filename)) {
/* Either the link worked or
* the copy succeeded. */
gkm_transaction_add (self, NULL,
complete_link_temporary,
result);
return TRUE;
}
}
g_free (result);
}
if (stat_failed && (errno == ENOENT || errno == ENOTDIR)) {
/* The original file does not exist */
*exists = FALSE;
return TRUE;
}
/* If exists, try again, otherwise fail */
if (errno != EEXIST) {
g_warning ("couldn't create temporary file for: %s: %s",
filename, g_strerror (errno));
gkm_transaction_fail (self, CKR_DEVICE_ERROR);
return FALSE;
}
}
g_assert_not_reached ();
}
static gboolean
write_sync_close (int fd, const guchar *data, gsize n_data)
{
int res;
if (fd == -1)
return FALSE;
while (n_data > 0) {
res = write (fd, data, n_data);
if (res < 0) {
if (errno != EINTR && errno != EAGAIN) {
close (fd);
return FALSE;
}
}
n_data -= MAX (res, n_data);
}
#ifdef HAVE_FSYNC
if (fsync (fd) < 0) {
close (fd);
return FALSE;
}
#endif
if (close (fd) < 0)
return FALSE;
return TRUE;
}
static gboolean
write_to_file (const gchar *filename, const guchar *data, gsize n_data)
{
gchar *dirname;
gchar *template;
gboolean result;
g_assert (filename);
dirname = g_path_get_dirname (filename);
template = g_build_filename (dirname, ".temp-XXXXXX", NULL);
g_free (dirname);
if (write_sync_close (g_mkstemp (template), data, n_data)) {
result = g_rename (template, filename) == 0;
} else {
g_unlink (template);
result = FALSE;
}
g_free (template);
return result;
}
/* -----------------------------------------------------------------------------
* OBJECT
*/
static gboolean
gkm_transaction_real_complete (GkmTransaction *self)
{
GList *l;
g_return_val_if_fail (!self->completed, FALSE);
self->completed = TRUE;
g_object_notify (G_OBJECT (self), "completed");
for (l = self->completes; l; l = g_list_next (l)) {
complete_invoke (self, l->data);
complete_destroy (l->data);
}
g_list_free (self->completes);
self->completes = NULL;
return TRUE;
}
static void
gkm_transaction_init (GkmTransaction *self)
{
}
static void
gkm_transaction_dispose (GObject *obj)
{
GkmTransaction *self = GKM_TRANSACTION (obj);
if (!self->completed)
gkm_transaction_complete (self);
G_OBJECT_CLASS (gkm_transaction_parent_class)->dispose (obj);
}
static void
gkm_transaction_finalize (GObject *obj)
{
GkmTransaction *self = GKM_TRANSACTION (obj);
g_assert (!self->completes);
g_assert (self->completed);
G_OBJECT_CLASS (gkm_transaction_parent_class)->finalize (obj);
}
static void
gkm_transaction_set_property (GObject *obj, guint prop_id, const GValue *value,
GParamSpec *pspec)
{
switch (prop_id) {
default:
G_OBJECT_WARN_INVALID_PROPERTY_ID (obj, prop_id, pspec);
break;
}
}
static void
gkm_transaction_get_property (GObject *obj, guint prop_id, GValue *value,
GParamSpec *pspec)
{
GkmTransaction *self = GKM_TRANSACTION (obj);
switch (prop_id) {
case PROP_COMPLETED:
g_value_set_boolean (value, gkm_transaction_get_completed (self));
break;
case PROP_FAILED:
g_value_set_boolean (value, gkm_transaction_get_failed (self));
break;
case PROP_RESULT:
g_value_set_ulong (value, gkm_transaction_get_result (self));
break;
default:
G_OBJECT_WARN_INVALID_PROPERTY_ID (obj, prop_id, pspec);
break;
}
}
static void
gkm_transaction_class_init (GkmTransactionClass *klass)
{
GObjectClass *gobject_class = G_OBJECT_CLASS (klass);
gobject_class->dispose = gkm_transaction_dispose;
gobject_class->finalize = gkm_transaction_finalize;
gobject_class->set_property = gkm_transaction_set_property;
gobject_class->get_property = gkm_transaction_get_property;
klass->complete = gkm_transaction_real_complete;
g_object_class_install_property (gobject_class, PROP_COMPLETED,
g_param_spec_boolean ("completed", "Completed", "Whether transaction is complete",
FALSE, G_PARAM_READABLE));
g_object_class_install_property (gobject_class, PROP_FAILED,
g_param_spec_boolean ("failed", "Failed", "Whether transaction failed",
FALSE, G_PARAM_READABLE));
g_object_class_install_property (gobject_class, PROP_RESULT,
g_param_spec_ulong ("result", "Result", "Result code for transaction",
0, G_MAXULONG, CKR_OK, G_PARAM_READABLE));
signals[COMPLETE] = g_signal_new ("complete", GKM_TYPE_TRANSACTION,
G_SIGNAL_RUN_LAST, G_STRUCT_OFFSET (GkmTransactionClass, complete),
complete_accumulator, NULL, gkm_marshal_BOOLEAN__VOID,
G_TYPE_BOOLEAN, 0, G_TYPE_NONE);
}
/* -----------------------------------------------------------------------------
* PUBLIC
*/
GkmTransaction*
gkm_transaction_new (void)
{
return g_object_new (GKM_TYPE_TRANSACTION, NULL);
}
void
gkm_transaction_add (GkmTransaction *self, gpointer object,
GkmTransactionFunc func, gpointer user_data)
{
Complete *complete;
g_return_if_fail (GKM_IS_TRANSACTION (self));
g_return_if_fail (func);
complete = g_slice_new0 (Complete);
complete->func = func;
if (object)
complete->object = g_object_ref (object);
complete->user_data = user_data;
self->completes = g_list_prepend (self->completes, complete);
}
void
gkm_transaction_fail (GkmTransaction *self, CK_RV result)
{
g_return_if_fail (GKM_IS_TRANSACTION (self));
g_return_if_fail (!self->completed);
g_return_if_fail (result != CKR_OK);
g_return_if_fail (!self->failed);
self->failed = TRUE;
self->result = result;
g_object_notify (G_OBJECT (self), "failed");
g_object_notify (G_OBJECT (self), "result");
}
void
gkm_transaction_complete(GkmTransaction *self)
{
gboolean critical = FALSE;
g_return_if_fail (GKM_IS_TRANSACTION (self));
g_return_if_fail (!self->completed);
g_signal_emit (self, signals[COMPLETE], 0, &critical);
g_assert (self->completed);
if (!self->failed && critical) {
g_warning ("transaction failed to commit, data may be lost");
self->failed = TRUE;
self->result = CKR_GENERAL_ERROR;
g_object_notify (G_OBJECT (self), "failed");
g_object_notify (G_OBJECT (self), "result");
}
}
gboolean
gkm_transaction_get_completed (GkmTransaction *self)
{
g_return_val_if_fail (GKM_IS_TRANSACTION (self), FALSE);
return self->completed;
}
gboolean
gkm_transaction_get_failed (GkmTransaction *self)
{
g_return_val_if_fail (GKM_IS_TRANSACTION (self), FALSE);
return self->failed;
}
CK_RV
gkm_transaction_get_result (GkmTransaction *self)
{
g_return_val_if_fail (GKM_IS_TRANSACTION (self), FALSE);
return self->result;
}
void
gkm_transaction_write_file (GkmTransaction *self, const gchar *filename,
gconstpointer data, gsize n_data)
{
gboolean exists;
g_return_if_fail (GKM_IS_TRANSACTION (self));
g_return_if_fail (filename);
g_return_if_fail (data);
g_return_if_fail (!gkm_transaction_get_failed (self));
if (!begin_link_temporary_if_exists (self, filename, &exists))
return;
if (!exists) {
if (!begin_new_file (self, filename))
return;
}
/* Put data in the expected place */
if (!write_to_file (filename, data, n_data)) {
g_warning ("couldn't write to file: %s: %s", filename, g_strerror (errno));
gkm_transaction_fail (self, CKR_DEVICE_ERROR);
}
}
gchar*
gkm_transaction_unique_file (GkmTransaction *self, const gchar *directory,
const gchar *basename)
{
gchar *ext;
gchar *filename = NULL;
gchar *base = NULL;
gchar *result = NULL;
gint seed = 1;
int fd;
g_return_val_if_fail (GKM_IS_TRANSACTION (self), NULL);
g_return_val_if_fail (directory, NULL);
g_return_val_if_fail (basename, NULL);
g_return_val_if_fail (!gkm_transaction_get_failed (self), NULL);
if (!g_mkdir_with_parents (directory, S_IRWXU) < 0) {
g_warning ("couldn't create directory: %s: %s", directory, g_strerror (errno));
gkm_transaction_fail (self, CKR_DEVICE_ERROR);
return NULL;
}
filename = g_build_filename (directory, basename, NULL);
/* Write a zero byte file */
fd = g_open (filename, O_RDONLY | O_CREAT | O_EXCL, S_IRUSR | S_IWUSR);
if (fd != -1) {
result = g_strdup (basename);
/* Try to find a unique filename */
} else if (errno == EEXIST) {
base = g_strdup (basename);
ext = strrchr (base, '.');
if (ext != NULL)
*(ext++) = '\0';
do {
g_free (result);
result = g_strdup_printf ("%s_%d%s%s", base, seed++,
ext ? "." : "", ext ? ext : "");
g_free (filename);
filename = g_build_filename (directory, result, NULL);
fd = g_open (filename, O_RDONLY | O_CREAT | O_EXCL, S_IRUSR | S_IWUSR);
} while (seed < MAX_TRIES && fd == -1 && errno == EEXIST);
}
/* Something failed */
if (fd == -1){
g_warning ("couldn't open file: %s: %s", filename, g_strerror (errno));
gkm_transaction_fail (self, CKR_DEVICE_ERROR);
/* Success, just leave our zero byte file */
} else {
gkm_transaction_add (self, NULL, complete_new_file, filename);
filename = NULL;
close (fd);
}
g_free (filename);
g_free (base);
if (gkm_transaction_get_failed (self)) {
g_free (result);
result = NULL;
}
return result;
}
void
gkm_transaction_remove_file (GkmTransaction *self, const gchar *filename)
{
gboolean exists;
g_return_if_fail (GKM_IS_TRANSACTION (self));
g_return_if_fail (filename);
g_return_if_fail (!gkm_transaction_get_failed (self));
if (!begin_link_temporary_if_exists (self, filename, &exists))
return;
/* Already gone? Job accomplished */
if (!exists)
return;
/* If failure, temporary will automatically be removed */
if (g_unlink (filename) < 0) {
g_warning ("couldn't remove file: %s: %s", filename, g_strerror (errno));
gkm_transaction_fail (self, CKR_DEVICE_ERROR);
}
}
CK_RV
gkm_transaction_complete_and_unref (GkmTransaction *self)
{
CK_RV rv;
g_return_val_if_fail (GKM_IS_TRANSACTION (self), CKR_GENERAL_ERROR);
gkm_transaction_complete (self);
rv = gkm_transaction_get_result (self);
g_object_unref (self);
return rv;
}