/*
 * LibGCab
 * Copyright (c) 2012, Marc-André Lureau <marcandre.lureau@gmail.com>
 * Copyright (c) 2017, Richard Hughes <richard@hughsie.com>
 *
 * 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.1 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., 51 Franklin Street, Fifth Floor, Boston,
 * MA 02110-1301 USA
 */

#include "config.h"

#include "gcab-priv.h"
#include "gcab-file.h"

/**
 * SECTION:gcab-file
 * @title: GCabFile
 * @short_description: A file contained in the Cabinet
 * @see_also: #GCabFolder
 * @stability: Stable
 * @include: libgcab.h
 *
 * A GCabFile is a handle to a file inside a Cabinet archive.
 * It can either be a file that is already within an exisiting
 * archive, or a file that reference a file on disk that will be used
 * for a new archive creation. In the later case, gcab_file_get_file()
 * must return a valid handle.
 */

struct _GCabFile
{
    GObject parent_instance;

    gchar *extract_name;
    GFile *file;
    GBytes *bytes;
    cfile_t *cfile;
};

enum {
    PROP_0,

    PROP_NAME,
    PROP_FILE,
    PROP_BYTES,
};

G_DEFINE_TYPE (GCabFile, gcab_file, G_TYPE_OBJECT);

static void
gcab_file_init (GCabFile *self)
{
}

static void
gcab_file_finalize (GObject *object)
{
    GCabFile *self = GCAB_FILE (object);

    if (self->file != NULL)
        g_object_unref (self->file);
    if (self->bytes != NULL)
        g_bytes_unref (self->bytes);
    cfile_free (self->cfile);
    g_free (self->extract_name);

    G_OBJECT_CLASS (gcab_file_parent_class)->finalize (object);
}

static void
gcab_file_set_name (GCabFile *self, const gchar *name)
{
    gchar *fname = g_strdup (name);

    g_return_if_fail (self->cfile != NULL);

    /* assuming that on win32 we don't get unix paths */
#ifndef G_OS_WIN32
    if (fname) {
        int i, len = strlen (fname);
        for (i = 0; i < len; i++)
            if (fname[i] == '/')
                fname[i] = '\\';
    }
#endif

    g_free (self->cfile->name);
    self->cfile->name = fname;
}

G_GNUC_INTERNAL void
gcab_file_set_bytes (GCabFile *self, GBytes *bytes)
{
    if (self->bytes != NULL)
        g_bytes_unref (self->bytes);
    self->bytes = g_bytes_ref (bytes);

    /* this is embedded into the archive */
    self->cfile->usize = g_bytes_get_size (bytes);
}

static void
gcab_file_set_property (GObject *object, guint prop_id, const GValue *value, GParamSpec *pspec)
{
    g_return_if_fail (GCAB_IS_FILE (object));
    GCabFile *self = GCAB_FILE (object);

    switch (prop_id) {
    case PROP_NAME:
        gcab_file_set_name (self, g_value_get_string (value));
        break;
    case PROP_FILE:
        self->file = g_value_dup_object (value);
        break;
    case PROP_BYTES:
        gcab_file_set_bytes (self, g_value_get_boxed (value));
        break;
    default:
        G_OBJECT_WARN_INVALID_PROPERTY_ID (object, prop_id, pspec);
        break;
    }
}

static void
gcab_file_get_property (GObject *object, guint prop_id, GValue *value, GParamSpec *pspec)
{
    g_return_if_fail (GCAB_IS_FILE (object));
    GCabFile *self = GCAB_FILE(object);

    switch (prop_id) {
    case PROP_NAME:
        g_value_set_string (value, self->cfile->name);
        break;
    case PROP_FILE:
        g_value_set_object (value, self->file);
        break;
    case PROP_BYTES:
        g_value_set_boxed (value, self->bytes);
        break;
    default:
        G_OBJECT_WARN_INVALID_PROPERTY_ID (object, prop_id, pspec);
        break;
    }
}

static void
gcab_file_class_init (GCabFileClass *klass)
{
    GObjectClass* object_class = G_OBJECT_CLASS (klass);

    object_class->finalize = gcab_file_finalize;
    object_class->set_property = gcab_file_set_property;
    object_class->get_property = gcab_file_get_property;

    g_object_class_install_property (object_class, PROP_NAME,
        g_param_spec_string ("name", "name", "name", NULL,
                             G_PARAM_READWRITE |
                             G_PARAM_STATIC_STRINGS));

    g_object_class_install_property (object_class, PROP_FILE,
        g_param_spec_object ("file", "file", "file", G_TYPE_FILE,
                             G_PARAM_READWRITE |
                             G_PARAM_STATIC_STRINGS));

    g_object_class_install_property (object_class, PROP_BYTES,
        g_param_spec_boxed ("bytes", "bytes", "bytes", G_TYPE_BYTES,
                            G_PARAM_READWRITE |
                            G_PARAM_STATIC_STRINGS));
}

/**
 * gcab_file_set_date:
 * @file: a #GCabFile
 * @tv: a #GTimeVal
 *
 * Sets the file modification date, instead of the value provided by the GFile.
 *
 * Since: 1.0
 **/
void
gcab_file_set_date (GCabFile *self, const GTimeVal *tv)
{
    g_autoptr(GDateTime) dt = g_date_time_new_from_timeval_utc (tv);
    self->cfile->date = ((g_date_time_get_year (dt) - 1980 ) << 9 ) +
        ((g_date_time_get_month (dt)) << 5) +
         (g_date_time_get_day_of_month (dt));
    self->cfile->time = ((g_date_time_get_hour (dt)) << 11) +
        (g_date_time_get_minute (dt) << 5) +
        (g_date_time_get_second (dt) / 2);
}

G_GNUC_INTERNAL gboolean
gcab_file_update_info (GCabFile *self, GFileInfo *info)
{
    GTimeVal tv;

    g_return_val_if_fail (GCAB_IS_FILE (self), FALSE);
    g_return_val_if_fail (G_IS_FILE_INFO (info), FALSE);

    g_file_info_get_modification_time (info, &tv);
    if (self->cfile->date == 0)
        gcab_file_set_date (self, &tv);
    self->cfile->usize = g_file_info_get_size (info);
    self->cfile->fattr = GCAB_FILE_ATTRIBUTE_ARCH;

    return TRUE;
}

G_GNUC_INTERNAL gboolean
gcab_file_set_uoffset (GCabFile *self, guint32 uoffset)
{
    g_return_val_if_fail (GCAB_IS_FILE (self), FALSE);

    self->cfile->uoffset = uoffset;

    return TRUE;
}

G_GNUC_INTERNAL guint32
gcab_file_get_uoffset (GCabFile *self)
{
    g_return_val_if_fail (GCAB_IS_FILE (self), 0);
    return self->cfile->uoffset;
}

G_GNUC_INTERNAL guint32
gcab_file_get_usize (GCabFile *self)
{
    g_return_val_if_fail (GCAB_IS_FILE (self), 0);
    return self->cfile->usize;
}

G_GNUC_INTERNAL GFile *
gcab_file_get_gfile (GCabFile *self)
{
    g_return_val_if_fail (GCAB_IS_FILE (self), NULL);
    return self->file;
}

G_GNUC_INTERNAL void
gcab_file_add_attribute (GCabFile *self, guint32 attribute)
{
    g_return_if_fail (GCAB_IS_FILE (self));
    self->cfile->fattr |= attribute;
}

G_GNUC_INTERNAL cfile_t *
gcab_file_get_cfile (GCabFile *self)
{
    g_return_val_if_fail (GCAB_IS_FILE (self), NULL);
    return self->cfile;
}

/**
 * gcab_file_get_size:
 * @file: a #GCabFile
 *
 * Get the file size.
 *
 * Returns: the cabinet file size
 * Since: 0.6
 **/
guint32
gcab_file_get_size (GCabFile *self)
{
    g_return_val_if_fail (GCAB_IS_FILE (self), 0);

    return self->cfile->usize;
}

/**
 * gcab_file_get_date:
 * @file: a #GCabFile
 * @result: a #GTimeVal to return date
 *
 * Get the file date, in @result.
 *
 * Since: 0.6
 *
 * Returns: %TRUE if @tv was set
 **/
gboolean
gcab_file_get_date (GCabFile *self, GTimeVal *tv)
{
    guint16 date, time;
    g_autoptr(GDateTime) dt = NULL;

    g_return_val_if_fail (GCAB_IS_FILE (self), FALSE);
    g_return_val_if_fail (tv != NULL, FALSE);

    date = self->cfile->date;
    time = self->cfile->time;
    dt = g_date_time_new_utc ((date >> 9) + 1980,
                              (date >> 5) & 0xf,
                              (date & 0x1f),
                              (time >> 11),
                              (time >> 5) & 0x3f,
                              (time & 0x1f) * 2);
    if (dt == NULL)
        return FALSE;
    return g_date_time_to_timeval (dt, tv);
}

/**
 * gcab_file_get_attributes:
 * @file: a #GCabFile
 *
 * Get the file attributes.
 *
 * Returns: the cabinet file attributes
 * Since: 0.6
 **/
guint32
gcab_file_get_attributes (GCabFile *self)
{
    g_return_val_if_fail (GCAB_IS_FILE (self), 0);

    return self->cfile->fattr;
}

/**
 * gcab_file_set_attributes:
 * @file: a #GCabFile
 * @attr: the attributes, e.g. %GCAB_FILE_ATTRIBUTE_RDONLY
 *
 * Set the file attributes.
 *
 * Since: 1.0
 **/
void
gcab_file_set_attributes (GCabFile *self, guint32 attr)
{
    g_return_if_fail (GCAB_IS_FILE (self));
    self->cfile->fattr = attr;
}

/**
 * gcab_file_get_name:
 * @file: a #GCabFile
 *
 * Get the file name within the cabinet.
 *
 * Returns: the cabinet file name
 **/
const gchar *
gcab_file_get_name (GCabFile *self)
{
    g_return_val_if_fail (GCAB_IS_FILE (self), NULL);

    return self->cfile->name;
}

/**
 * gcab_file_get_file:
 * @file: a #GCabFile
 *
 * If the cabinet is being created, get the #GFile associated with
 * @file. This must be an exisiting file that can be read, in order to
 * be added to the archive during cabinet creation.
 *
 * If @file is from an existing cabinet, the fuction will return
 * %NULL.
 *
 * Returns: (transfer none): the associated #GFile or %NULL
 **/
GFile *
gcab_file_get_file (GCabFile *self)
{
    g_return_val_if_fail (GCAB_IS_FILE (self), NULL);

    return self->file;
}

/**
 * gcab_file_get_bytes:
 * @file: a #GCabFile
 *
 * Get the #GFile associated with @file. This will only be non-%NULL if the
 * #GCabFile has been created using gcab_file_new_with_bytes().
 *
 * Returns: (transfer none): the associated #GBytes or %NULL
 *
 * Since: 1.0
 **/
GBytes *
gcab_file_get_bytes (GCabFile *self)
{
    g_return_val_if_fail (GCAB_IS_FILE (self), NULL);
    return self->bytes;
}

/**
 * gcab_file_new_with_file:
 * @name: name of the file within the cabinet
 * @file: a #GFile to be added to the cabinet
 *
 * Create a #GCabFile from a given #GFile, to be added to a
 * #GCabCabinet for archive creation.
 *
 * Returns: a new #GCabFile
 **/
GCabFile *
gcab_file_new_with_file (const gchar *name, GFile *file)
{
    g_return_val_if_fail (name != NULL, NULL);
    g_return_val_if_fail (G_IS_FILE (file), NULL);

    GCabFile *self = g_object_new (GCAB_TYPE_FILE,
                                   "file", file,
                                   NULL);
    self->cfile = g_new0 (cfile_t, 1);
    gcab_file_set_name (self, name);
    return self;
}

/**
 * gcab_file_new_with_bytes:
 * @name: name of the file within the cabinet
 * @bytes: a #GBytes to be added to the cabinet
 *
 * Create a #GCabFile from a given #GBytes.
 *
 * If this file is to be added to an archive you should also probably use
 * gcab_file_set_date() and gcab_file_set_attributes() to set sensible values.
 *
 * Returns: a new #GCabFile
 *
 * Since: 1.0
 **/
GCabFile *
gcab_file_new_with_bytes (const gchar *name, GBytes *bytes)
{
    g_return_val_if_fail (name != NULL, NULL);
    g_return_val_if_fail (bytes != NULL, NULL);

    GCabFile *self = g_object_new (GCAB_TYPE_FILE, NULL);
    self->cfile = g_new0 (cfile_t, 1);
    gcab_file_set_bytes (self, bytes);
    gcab_file_set_name (self, name);
    return self;
}

G_GNUC_INTERNAL GCabFile *
gcab_file_new_steal_cfile (cfile_t **cfile)
{
    g_return_val_if_fail (cfile != NULL, NULL);

    GCabFile *file = g_object_new (GCAB_TYPE_FILE, NULL);
    file->cfile = g_steal_pointer (cfile);

    return file;
}

G_GNUC_INTERNAL GInputStream *
gcab_file_get_input_stream (GCabFile *self, GCancellable *cancellable, GError **error)
{
    /* backed by a GFile */
    if (self->file != NULL)
        return G_INPUT_STREAM (g_file_read (self->file, cancellable, error));

    /* backed by a GBytes */
    if (self->bytes != NULL)
        return g_memory_input_stream_new_from_bytes (self->bytes);

    /* nothing to do */
    g_set_error (error, GCAB_ERROR, GCAB_ERROR_FORMAT,
                 "No GFile for %s", gcab_file_get_name (self));
    return NULL;
}

G_GNUC_INTERNAL GOutputStream *
gcab_file_get_output_stream (GCabFile *self,
                             GFile *path_extract,
                             GCancellable *cancellable,
                             GError **error)
{
    /* not writing to a GFile */
    if (path_extract == NULL)
        return g_memory_output_stream_new_resizable ();

    /* make path have UNIX directory slashes */
    g_autofree gchar *fname = g_strdup (gcab_file_get_extract_name (self));
    g_strdelimit (fname, "\\", '/');

    /* "Rebase" the file in the given path, to ensure we never escape it */
    g_autoptr(GFile) file = g_file_resolve_relative_path (path_extract, fname);
    if (!g_file_has_prefix (file, path_extract)) {
        g_autofree gchar *rawpath = g_file_get_path (file);
        if (rawpath != NULL) {
            char *newpath = rawpath;
            while (*newpath != 0 && *newpath == G_DIR_SEPARATOR) {
                newpath++;
            }
            g_autoptr(GFile) newfile = g_file_resolve_relative_path (path_extract, newpath);
            g_set_object (&file, newfile);
        }
    }

    /* create parent directories */
    g_autoptr(GFile) parent = g_file_get_parent (file);
    g_autoptr(GError) error_local = NULL;
    if (!g_file_make_directory_with_parents (parent, cancellable, &error_local)) {
        if (!g_error_matches (error_local, G_IO_ERROR, G_IO_ERROR_EXISTS)) {
            g_propagate_error (error, g_steal_pointer (&error_local));
            return NULL;
        }
    }

    /* write to a file */
    return G_OUTPUT_STREAM (g_file_replace (file, NULL, FALSE,
                                            G_FILE_CREATE_REPLACE_DESTINATION,
                                            cancellable, error));
}

/**
 * gcab_file_get_extract_name:
 * @file: a #GCabFile
 *
 * Get the file name to use for extraction, or %NULL.
 *
 * Returns: (allow-none): a file name
 **/
const gchar *
gcab_file_get_extract_name (GCabFile *self)
{
    g_return_val_if_fail (GCAB_IS_FILE (self), NULL);

    return self->extract_name ? self->extract_name : self->cfile->name;
}

/**
 * gcab_file_set_extract_name:
 * @file: a #GCabFile
 * @name: (allow-none): a file name or %NULL
 *
 * Sets the file name to use for extraction, instead of the name
 * provided by the Cabinet.
 **/
void
gcab_file_set_extract_name (GCabFile *self, const gchar *name)
{
    g_return_if_fail (GCAB_IS_FILE (self));

    g_free (self->extract_name);
    self->extract_name = g_strdup (name);
}
