/*
 * Copyright (C) 2014 Colin Walters <walters@verbum.org>
 *
 * SPDX-License-Identifier: LGPL-2.0+
 *
 * 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, see <https://www.gnu.org/licenses/>.
 */

#include "config.h"

#include "otutil.h"

#include "ostree-core-private.h"
#include "ostree-sysroot-upgrader.h"
#include "ostree.h"

/**
 * SECTION:ostree-sysroot-upgrader
 * @title: Simple upgrade class
 * @short_description: Upgrade OSTree systems
 *
 * The #OstreeSysrootUpgrader class allows performing simple upgrade
 * operations.
 */
typedef struct
{
  GObjectClass parent_class;
} OstreeSysrootUpgraderClass;

struct OstreeSysrootUpgrader
{
  GObject parent;

  OstreeSysroot *sysroot;
  char *osname;
  OstreeSysrootUpgraderFlags flags;

  OstreeDeployment *merge_deployment;
  GKeyFile *origin;
  char *origin_remote;
  char *origin_ref;
  char *override_csum;

  char *new_revision;
};

enum
{
  PROP_0,

  PROP_SYSROOT,
  PROP_OSNAME,
  PROP_FLAGS
};

static void ostree_sysroot_upgrader_initable_iface_init (GInitableIface *iface);

G_DEFINE_TYPE_WITH_CODE (OstreeSysrootUpgrader, ostree_sysroot_upgrader, G_TYPE_OBJECT,
                         G_IMPLEMENT_INTERFACE (G_TYPE_INITABLE,
                                                ostree_sysroot_upgrader_initable_iface_init))

static gboolean
parse_refspec (OstreeSysrootUpgrader *self, GCancellable *cancellable, GError **error)
{
  g_autofree char *origin_refspec = NULL;
  g_autofree char *unconfigured_state = NULL;
  g_autofree char *csum = NULL;

  if ((self->flags & OSTREE_SYSROOT_UPGRADER_FLAGS_IGNORE_UNCONFIGURED) == 0)
    {
      /* If explicit action by the OS creator is requried to upgrade, print their text as an error.
       * NOTE: If changing this, see the matching implementation in ostree-repo-pull.c.
       */
      unconfigured_state
          = g_key_file_get_string (self->origin, "origin", "unconfigured-state", NULL);
      if (unconfigured_state)
        return glnx_throw (error, "origin unconfigured-state: %s", unconfigured_state);
    }

  origin_refspec = g_key_file_get_string (self->origin, "origin", "refspec", NULL);
  if (!origin_refspec)
    return glnx_throw (error,
                       "No origin/refspec in current deployment origin; cannot upgrade via ostree");

  g_clear_pointer (&self->origin_remote, g_free);
  g_clear_pointer (&self->origin_ref, g_free);
  if (!ostree_parse_refspec (origin_refspec, &self->origin_remote, &self->origin_ref, error))
    return FALSE;

  csum = g_key_file_get_string (self->origin, "origin", "override-commit", NULL);
  if (csum != NULL && !ostree_validate_checksum_string (csum, error))
    return FALSE;
  g_clear_pointer (&self->override_csum, g_free);
  self->override_csum = g_steal_pointer (&csum);

  return TRUE;
}

static gboolean
ostree_sysroot_upgrader_initable_init (GInitable *initable, GCancellable *cancellable,
                                       GError **error)
{
  OstreeSysrootUpgrader *self = (OstreeSysrootUpgrader *)initable;
  OstreeDeployment *booted_deployment = ostree_sysroot_get_booted_deployment (self->sysroot);

  if (booted_deployment == NULL && self->osname == NULL)
    return glnx_throw (error, "Not currently booted into an OSTree system and no OS specified");

  if (self->osname == NULL)
    {
      g_assert (booted_deployment);
      self->osname = g_strdup (ostree_deployment_get_osname (booted_deployment));
    }
  else if (self->osname[0] == '\0')
    return glnx_throw (error, "Invalid empty osname");

  self->merge_deployment = ostree_sysroot_get_merge_deployment (self->sysroot, self->osname);
  if (self->merge_deployment == NULL)
    return glnx_throw (error, "No previous deployment for OS '%s'", self->osname);

  self->origin = ostree_deployment_get_origin (self->merge_deployment);
  if (!self->origin)
    return glnx_throw (error, "No origin known for deployment %s.%d",
                       ostree_deployment_get_csum (self->merge_deployment),
                       ostree_deployment_get_deployserial (self->merge_deployment));
  g_key_file_ref (self->origin);

  if (!parse_refspec (self, cancellable, error))
    return FALSE;

  return TRUE;
}

static void
ostree_sysroot_upgrader_initable_iface_init (GInitableIface *iface)
{
  iface->init = ostree_sysroot_upgrader_initable_init;
}

static void
ostree_sysroot_upgrader_finalize (GObject *object)
{
  OstreeSysrootUpgrader *self = OSTREE_SYSROOT_UPGRADER (object);

  g_clear_object (&self->sysroot);
  g_free (self->osname);

  g_clear_object (&self->merge_deployment);
  if (self->origin)
    g_key_file_unref (self->origin);
  g_free (self->origin_remote);
  g_free (self->origin_ref);
  g_free (self->override_csum);
  g_free (self->new_revision);

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

static void
ostree_sysroot_upgrader_set_property (GObject *object, guint prop_id, const GValue *value,
                                      GParamSpec *pspec)
{
  OstreeSysrootUpgrader *self = OSTREE_SYSROOT_UPGRADER (object);

  switch (prop_id)
    {
    case PROP_SYSROOT:
      self->sysroot = g_value_dup_object (value);
      break;
    case PROP_OSNAME:
      self->osname = g_value_dup_string (value);
      break;
    case PROP_FLAGS:
      self->flags = g_value_get_flags (value);
      break;
    default:
      G_OBJECT_WARN_INVALID_PROPERTY_ID (object, prop_id, pspec);
      break;
    }
}

static void
ostree_sysroot_upgrader_get_property (GObject *object, guint prop_id, GValue *value,
                                      GParamSpec *pspec)
{
  OstreeSysrootUpgrader *self = OSTREE_SYSROOT_UPGRADER (object);

  switch (prop_id)
    {
    case PROP_SYSROOT:
      g_value_set_object (value, self->sysroot);
      break;
    case PROP_OSNAME:
      g_value_set_string (value, self->osname);
      break;
    case PROP_FLAGS:
      g_value_set_flags (value, self->flags);
      break;
    default:
      G_OBJECT_WARN_INVALID_PROPERTY_ID (object, prop_id, pspec);
      break;
    }
}

static void
ostree_sysroot_upgrader_constructed (GObject *object)
{
  OstreeSysrootUpgrader *self = OSTREE_SYSROOT_UPGRADER (object);

  g_assert (self->sysroot != NULL);

  G_OBJECT_CLASS (ostree_sysroot_upgrader_parent_class)->constructed (object);
}

static void
ostree_sysroot_upgrader_class_init (OstreeSysrootUpgraderClass *klass)
{
  GObjectClass *object_class = G_OBJECT_CLASS (klass);

  object_class->constructed = ostree_sysroot_upgrader_constructed;
  object_class->get_property = ostree_sysroot_upgrader_get_property;
  object_class->set_property = ostree_sysroot_upgrader_set_property;
  object_class->finalize = ostree_sysroot_upgrader_finalize;

  g_object_class_install_property (
      object_class, PROP_SYSROOT,
      g_param_spec_object ("sysroot", "", "", OSTREE_TYPE_SYSROOT,
                           G_PARAM_READWRITE | G_PARAM_CONSTRUCT_ONLY));

  g_object_class_install_property (
      object_class, PROP_OSNAME,
      g_param_spec_string ("osname", "", "", NULL, G_PARAM_READWRITE | G_PARAM_CONSTRUCT_ONLY));

  g_object_class_install_property (object_class, PROP_FLAGS,
                                   g_param_spec_flags ("flags", "", "",
                                                       ostree_sysroot_upgrader_flags_get_type (), 0,
                                                       G_PARAM_READWRITE | G_PARAM_CONSTRUCT_ONLY));
}

static void
ostree_sysroot_upgrader_init (OstreeSysrootUpgrader *self)
{
}

/**
 * ostree_sysroot_upgrader_new:
 * @sysroot: An #OstreeSysroot
 * @cancellable: Cancellable
 * @error: Error
 *
 * Returns: (transfer full): An upgrader
 */
OstreeSysrootUpgrader *
ostree_sysroot_upgrader_new (OstreeSysroot *sysroot, GCancellable *cancellable, GError **error)
{
  return g_initable_new (OSTREE_TYPE_SYSROOT_UPGRADER, cancellable, error, "sysroot", sysroot,
                         NULL);
}

/**
 * ostree_sysroot_upgrader_new_for_os:
 * @sysroot: An #OstreeSysroot
 * @osname: (allow-none): Operating system name
 * @cancellable: Cancellable
 * @error: Error
 *
 * Returns: (transfer full): An upgrader
 */
OstreeSysrootUpgrader *
ostree_sysroot_upgrader_new_for_os (OstreeSysroot *sysroot, const char *osname,
                                    GCancellable *cancellable, GError **error)
{
  return g_initable_new (OSTREE_TYPE_SYSROOT_UPGRADER, cancellable, error, "sysroot", sysroot,
                         "osname", osname, NULL);
}

/**
 * ostree_sysroot_upgrader_new_for_os_with_flags:
 * @sysroot: An #OstreeSysroot
 * @osname: (allow-none): Operating system name
 * @flags: Flags
 * @cancellable: Cancellable
 * @error: Error
 *
 * Returns: (transfer full): An upgrader
 */
OstreeSysrootUpgrader *
ostree_sysroot_upgrader_new_for_os_with_flags (OstreeSysroot *sysroot, const char *osname,
                                               OstreeSysrootUpgraderFlags flags,
                                               GCancellable *cancellable, GError **error)
{
  return g_initable_new (OSTREE_TYPE_SYSROOT_UPGRADER, cancellable, error, "sysroot", sysroot,
                         "osname", osname, "flags", flags, NULL);
}

/**
 * ostree_sysroot_upgrader_get_origin:
 * @self: Sysroot
 *
 * Returns: (transfer none) (nullable): The origin file, or %NULL if unknown.
 */
GKeyFile *
ostree_sysroot_upgrader_get_origin (OstreeSysrootUpgrader *self)
{
  return self->origin;
}

/**
 * ostree_sysroot_upgrader_dup_origin:
 * @self: Sysroot
 *
 * Returns: (transfer full) (nullable): A copy of the origin file, or %NULL if unknown.
 */
GKeyFile *
ostree_sysroot_upgrader_dup_origin (OstreeSysrootUpgrader *self)
{
  g_assert (OSTREE_IS_SYSROOT_UPGRADER (self));

  GKeyFile *copy = NULL;

  if (self->origin != NULL)
    {
      g_autofree char *data = NULL;
      gsize length = 0;

      copy = g_key_file_new ();
      data = g_key_file_to_data (self->origin, &length, NULL);
      g_key_file_load_from_data (copy, data, length, G_KEY_FILE_KEEP_COMMENTS, NULL);
    }

  return copy;
}

/**
 * ostree_sysroot_upgrader_set_origin:
 * @self: Sysroot
 * @origin: (allow-none): The new origin
 * @cancellable: Cancellable
 * @error: Error
 *
 * Replace the origin with @origin.
 */
gboolean
ostree_sysroot_upgrader_set_origin (OstreeSysrootUpgrader *self, GKeyFile *origin,
                                    GCancellable *cancellable, GError **error)
{
  g_clear_pointer (&self->origin, g_key_file_unref);
  if (origin)
    {
      self->origin = g_key_file_ref (origin);
      if (!parse_refspec (self, cancellable, error))
        return FALSE;
    }

  return TRUE;
}

/**
 * ostree_sysroot_upgrader_get_origin_description:
 * @self: Upgrader
 *
 * Returns: (transfer full) (nullable): A one-line descriptive summary of the origin, or %NULL if
 * unknown.
 */
char *
ostree_sysroot_upgrader_get_origin_description (OstreeSysrootUpgrader *self)
{
  if (!self->origin)
    return NULL;
  return g_key_file_get_string (self->origin, "origin", "refspec", NULL);
}

/**
 * ostree_sysroot_upgrader_check_timestamps:
 * @repo: Repo
 * @from_rev: From revision
 * @to_rev: To revision
 * @error: Error
 *
 * Check that the timestamp on @to_rev is equal to or newer than
 * @from_rev.  This protects systems against man-in-the-middle
 * attackers which provide a client with an older commit.
 */
gboolean
ostree_sysroot_upgrader_check_timestamps (OstreeRepo *repo, const char *from_rev,
                                          const char *to_rev, GError **error)
{
  g_autoptr (GVariant) old_commit = NULL;
  if (!ostree_repo_load_variant (repo, OSTREE_OBJECT_TYPE_COMMIT, from_rev, &old_commit, error))
    return FALSE;

  g_autoptr (GVariant) new_commit = NULL;
  if (!ostree_repo_load_variant (repo, OSTREE_OBJECT_TYPE_COMMIT, to_rev, &new_commit, error))
    return FALSE;

  if (!_ostree_compare_timestamps (from_rev, ostree_commit_get_timestamp (old_commit), to_rev,
                                   ostree_commit_get_timestamp (new_commit), error))
    return FALSE;

  return TRUE;
}

/**
 * ostree_sysroot_upgrader_pull:
 * @self: Upgrader
 * @flags: Flags controlling pull behavior
 * @upgrader_flags: Flags controlling upgrader behavior
 * @progress: (allow-none): Progress
 * @out_changed: (out): Whether or not the origin changed
 * @cancellable: Cancellable
 * @error: Error
 *
 * Perform a pull from the origin.  First check if the ref has
 * changed, if so download the linked objects, and store the updated
 * ref locally.  Then @out_changed will be %TRUE.
 *
 * If the origin remote is unchanged, @out_changed will be set to
 * %FALSE.
 */
gboolean
ostree_sysroot_upgrader_pull (OstreeSysrootUpgrader *self, OstreeRepoPullFlags flags,
                              OstreeSysrootUpgraderPullFlags upgrader_flags,
                              OstreeAsyncProgress *progress, gboolean *out_changed,
                              GCancellable *cancellable, GError **error)
{
  return ostree_sysroot_upgrader_pull_one_dir (self, NULL, flags, upgrader_flags, progress,
                                               out_changed, cancellable, error);
}

/**
 * ostree_sysroot_upgrader_pull_one_dir:
 * @self: Upgrader
 * @dir_to_pull: Subdirectory path (should include a leading /)
 * @flags: Flags controlling pull behavior
 * @upgrader_flags: Flags controlling upgrader behavior
 * @progress: (allow-none): Progress
 * @out_changed: (out): Whether or not the origin changed
 * @cancellable: Cancellable
 * @error: Error
 *
 * Like ostree_sysroot_upgrader_pull(), but allows retrieving just a
 * subpath of the tree.  This can be used to download metadata files
 * from inside the tree such as package databases.
 *
 */
gboolean
ostree_sysroot_upgrader_pull_one_dir (OstreeSysrootUpgrader *self, const char *dir_to_pull,
                                      OstreeRepoPullFlags flags,
                                      OstreeSysrootUpgraderPullFlags upgrader_flags,
                                      OstreeAsyncProgress *progress, gboolean *out_changed,
                                      GCancellable *cancellable, GError **error)
{
  char *refs_to_fetch[] = { NULL, NULL };
  const char *from_revision = NULL;
  g_autofree char *origin_refspec = NULL;
  g_autofree char *new_revision = NULL;
  g_autoptr (GVariant) new_variant = NULL;
  g_autoptr (GVariant) new_metadata = NULL;
  g_autoptr (GVariant) rebase = NULL;

  if (self->override_csum != NULL)
    refs_to_fetch[0] = self->override_csum;
  else
    refs_to_fetch[0] = self->origin_ref;

  OstreeRepo *repo = ostree_sysroot_repo (self->sysroot);

  if (self->origin_remote)
    origin_refspec = g_strconcat (self->origin_remote, ":", self->origin_ref, NULL);
  else
    origin_refspec = g_strdup (self->origin_ref);

  g_assert (self->merge_deployment);
  from_revision = ostree_deployment_get_csum (self->merge_deployment);

  if (self->origin_remote && (upgrader_flags & OSTREE_SYSROOT_UPGRADER_PULL_FLAGS_SYNTHETIC) == 0)
    {
      g_autoptr (GVariantBuilder) optbuilder = g_variant_builder_new (G_VARIANT_TYPE ("a{sv}"));
      if (dir_to_pull && *dir_to_pull)
        g_variant_builder_add (optbuilder, "{s@v}", "subdir",
                               g_variant_new_variant (g_variant_new_string (dir_to_pull)));
      g_variant_builder_add (optbuilder, "{s@v}", "flags",
                             g_variant_new_variant (g_variant_new_int32 (flags)));
      /* Add the timestamp check, unless disabled */
      if ((upgrader_flags & OSTREE_SYSROOT_UPGRADER_PULL_FLAGS_ALLOW_OLDER) == 0)
        g_variant_builder_add (optbuilder, "{s@v}", "timestamp-check-from-rev",
                               g_variant_new_variant (g_variant_new_string (from_revision)));

      g_variant_builder_add (
          optbuilder, "{s@v}", "refs",
          g_variant_new_variant (g_variant_new_strv ((const char *const *)refs_to_fetch, -1)));
      g_autoptr (GVariant) opts = g_variant_ref_sink (g_variant_builder_end (optbuilder));
      if (!ostree_repo_pull_with_options (repo, self->origin_remote, opts, progress, cancellable,
                                          error))
        return FALSE;

      if (progress)
        ostree_async_progress_finish (progress);
    }

  /* Check to see if the commit marks the ref as EOL, redirecting to
   * another. */
  if (!ostree_repo_resolve_rev (repo, origin_refspec, FALSE, &new_revision, error))
    return FALSE;

  if (!ostree_repo_load_variant (repo, OSTREE_OBJECT_TYPE_COMMIT, new_revision, &new_variant,
                                 error))
    return FALSE;

  g_variant_get_child (new_variant, 0, "@a{sv}", &new_metadata);
  rebase = g_variant_lookup_value (new_metadata, OSTREE_COMMIT_META_KEY_ENDOFLIFE_REBASE,
                                   G_VARIANT_TYPE_STRING);
  if (rebase)
    {
      const char *new_ref = g_variant_get_string (rebase, 0);

      /* Pull the new ref */
      if (self->origin_remote
          && (upgrader_flags & OSTREE_SYSROOT_UPGRADER_PULL_FLAGS_SYNTHETIC) == 0)
        {
          refs_to_fetch[0] = (char *)new_ref;
          if (!ostree_repo_pull_one_dir (repo, self->origin_remote, dir_to_pull, refs_to_fetch,
                                         flags, progress, cancellable, error))
            return FALSE;
        }

      /* Use the new ref for the rest of the update process */
      g_free (self->origin_ref);
      self->origin_ref = g_strdup (new_ref);
      g_free (origin_refspec);

      if (self->origin_remote)
        origin_refspec = g_strconcat (self->origin_remote, ":", new_ref, NULL);
      else
        origin_refspec = g_strdup (new_ref);

      g_key_file_set_string (self->origin, "origin", "refspec", origin_refspec);
    }

  if (self->override_csum != NULL)
    {
      if (!ostree_repo_set_ref_immediate (repo, self->origin_remote, self->origin_ref,
                                          self->override_csum, cancellable, error))
        return FALSE;

      self->new_revision = g_strdup (self->override_csum);
    }
  else
    {
      if (!ostree_repo_resolve_rev (repo, origin_refspec, FALSE, &self->new_revision, error))
        return FALSE;
    }

  if (g_strcmp0 (from_revision, self->new_revision) == 0)
    {
      *out_changed = FALSE;
    }
  else
    {
      gboolean allow_older = (upgrader_flags & OSTREE_SYSROOT_UPGRADER_PULL_FLAGS_ALLOW_OLDER) > 0;

      *out_changed = TRUE;

      if (from_revision && !allow_older)
        {
          if (!ostree_sysroot_upgrader_check_timestamps (repo, from_revision, self->new_revision,
                                                         error))
            return FALSE;
        }
    }

  return TRUE;
}

/**
 * ostree_sysroot_upgrader_deploy:
 * @self: Self
 * @cancellable: Cancellable
 * @error: Error
 *
 * Write the new deployment to disk, perform a configuration merge
 * with /etc, and update the bootloader configuration.
 */
gboolean
ostree_sysroot_upgrader_deploy (OstreeSysrootUpgrader *self, GCancellable *cancellable,
                                GError **error)
{
  g_autoptr (OstreeDeployment) new_deployment = NULL;

  /* Experimental flag to enable staging */
  gboolean stage = (self->flags & OSTREE_SYSROOT_UPGRADER_FLAGS_STAGE) > 0
                   || getenv ("OSTREE_EX_STAGE_DEPLOYMENTS") != NULL;
  if (stage)
    {
      if (!ostree_sysroot_stage_tree (self->sysroot, self->osname, self->new_revision, self->origin,
                                      self->merge_deployment, NULL, &new_deployment, cancellable,
                                      error))
        return FALSE;
    }
  else
    {
      if (!ostree_sysroot_deploy_tree (self->sysroot, NULL, self->osname, self->new_revision,
                                       self->origin, self->merge_deployment, NULL, &new_deployment,
                                       cancellable, error))
        return FALSE;

      if (!ostree_sysroot_simple_write_deployment (self->sysroot, NULL,self->osname, new_deployment,
                                                   self->merge_deployment, 0, cancellable, error))
        return FALSE;
    }

  return TRUE;
}

GType
ostree_sysroot_upgrader_flags_get_type (void)
{
  static gsize static_g_define_type_id = 0;

  if (g_once_init_enter (&static_g_define_type_id))
    {
      static const GFlagsValue values[]
          = { { OSTREE_SYSROOT_UPGRADER_FLAGS_IGNORE_UNCONFIGURED,
                "OSTREE_SYSROOT_UPGRADER_FLAGS_IGNORE_UNCONFIGURED", "ignore-unconfigured" },
              { OSTREE_SYSROOT_UPGRADER_FLAGS_STAGE, "OSTREE_SYSROOT_UPGRADER_FLAGS_STAGE",
                "stage" },
              { 0, NULL, NULL } };
      GType g_define_type_id
          = g_flags_register_static (g_intern_static_string ("OstreeSysrootUpgraderFlags"), values);
      g_once_init_leave (&static_g_define_type_id, g_define_type_id);
    }

  return static_g_define_type_id;
}
