/*
 * Copyright © 2016 Kinvolk GmbH
 * Copyright © 2017 Endless Mobile, Inc.
 * Copyright © 2022 Igalia S.L.
 *
 * 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/>.
 *
 * Authors:
 *  - Krzesimir Nowak <krzesimir@kinvolk.io>
 *  - Philip Withnall <withnall@endlessm.com>
 */

#include "config.h"

#ifdef HAVE_AVAHI
#include <avahi-client/client.h>
#include <avahi-client/lookup.h>
#include <avahi-common/address.h>
#include <avahi-common/defs.h>
#include <avahi-common/error.h>
#include <avahi-common/malloc.h>
#include <avahi-common/strlst.h>
#include <avahi-glib/glib-malloc.h>
#include <avahi-glib/glib-watch.h>
#include <netinet/in.h>
#include <string.h>
#endif  /* HAVE_AVAHI */

#include <gio/gio.h>
#include <glib.h>
#include <glib-object.h>
#include <libglnx.h>

#include "ostree-autocleanups.h"
#include "ostree-repo-finder.h"
#include "ostree-repo-finder-avahi.h"

#ifdef HAVE_AVAHI
#include "ostree-bloom-private.h"
#include "ostree-remote-private.h"
#include "ostree-repo-private.h"
#include "ostree-repo.h"
#include "ostree-repo-finder-avahi-private.h"
#include "otutil.h"
#endif  /* HAVE_AVAHI */

/**
 * SECTION:ostree-repo-finder-avahi
 * @title: OstreeRepoFinderAvahi
 * @short_description: Finds remote repositories from ref names by looking at
 *    adverts of refs from peers on the local network
 * @stability: Unstable
 * @include: libostree/ostree-repo-finder-avahi.h
 *
 * #OstreeRepoFinderAvahi is an implementation of #OstreeRepoFinder which looks
 * for refs being hosted by peers on the local network.
 *
 * Any ref which matches by collection ID and ref name is returned as a result,
 * with no limitations on the peers which host them, as long as they are
 * accessible over the local network, and their adverts reach this machine via
 * DNS-SD/mDNS.
 *
 * For each repository which is found, a result will be returned for the
 * intersection of the refs being searched for, and the refs in `refs/mirrors`
 * in the remote repository.
 *
 * DNS-SD resolution is performed using Avahi, which will continue to scan for
 * matching peers throughout the lifetime of the process. It’s recommended that
 * ostree_repo_finder_avahi_start() be called early on in the process’ lifetime,
 * and the #GMainContext which is passed to ostree_repo_finder_avahi_new()
 * continues to be iterated until ostree_repo_finder_avahi_stop() is called.
 *
 * The values stored in DNS-SD TXT records are stored as big-endian whenever
 * endianness is relevant.
 *
 * Internally, #OstreeRepoFinderAvahi has an Avahi client, browser and resolver
 * which work in the background to track all available peers on the local
 * network. Whenever a resolve request is made using
 * ostree_repo_finder_resolve_async(), the request is blocked until the
 * background tracking is in a consistent state (typically this only happens at
 * startup), and is then answered using the current cache of background data.
 * The Avahi client tracks the #OstreeRepoFinderAvahi’s connection with the
 * Avahi D-Bus service. The browser looks for DNS-SD peers on the local network;
 * and the resolver is used to retrieve information about services advertised by
 * each peer, including the services’ TXT records.
 *
 * Since: 2018.6
 */

#ifdef HAVE_AVAHI
/* FIXME: Submit these upstream */
G_DEFINE_AUTOPTR_CLEANUP_FUNC (AvahiClient, avahi_client_free)
G_DEFINE_AUTOPTR_CLEANUP_FUNC (AvahiServiceBrowser, avahi_service_browser_free)
G_DEFINE_AUTOPTR_CLEANUP_FUNC (AvahiServiceResolver, avahi_service_resolver_free)

/* FIXME: Register this with IANA? https://tools.ietf.org/html/rfc6335#section-5.2 */
const gchar * const OSTREE_AVAHI_SERVICE_TYPE = "_ostree_repo._tcp";

static const gchar *
ostree_avahi_client_state_to_string (AvahiClientState state)
{
  switch (state)
    {
    case AVAHI_CLIENT_S_REGISTERING:
      return "registering";
    case AVAHI_CLIENT_S_RUNNING:
      return "running";
    case AVAHI_CLIENT_S_COLLISION:
      return "collision";
    case AVAHI_CLIENT_CONNECTING:
      return "connecting";
    case AVAHI_CLIENT_FAILURE:
      return "failure";
    default:
      return "unknown";
    }
}

static const gchar *
ostree_avahi_resolver_event_to_string (AvahiResolverEvent event)
{
  switch (event)
    {
    case AVAHI_RESOLVER_FOUND:
      return "found";
    case AVAHI_RESOLVER_FAILURE:
      return "failure";
    default:
      return "unknown";
    }
}

static const gchar *
ostree_avahi_browser_event_to_string (AvahiBrowserEvent event)
{
  switch (event)
    {
    case AVAHI_BROWSER_NEW:
      return "new";
    case AVAHI_BROWSER_REMOVE:
      return "remove";
    case AVAHI_BROWSER_CACHE_EXHAUSTED:
      return "cache-exhausted";
    case AVAHI_BROWSER_ALL_FOR_NOW:
      return "all-for-now";
    case AVAHI_BROWSER_FAILURE:
      return "failure";
    default:
      return "unknown";
    }
}

typedef struct
{
  gchar *uri;
  OstreeRemote *keyring_remote;  /* (owned) */
} UriAndKeyring;

static void
uri_and_keyring_free (UriAndKeyring *data)
{
  g_free (data->uri);
  ostree_remote_unref (data->keyring_remote);
  g_free (data);
}

G_DEFINE_AUTOPTR_CLEANUP_FUNC (UriAndKeyring, uri_and_keyring_free)

static UriAndKeyring *
uri_and_keyring_new (const gchar  *uri,
                     OstreeRemote *keyring_remote)
{
  g_autoptr(UriAndKeyring) data = NULL;

  data = g_new0 (UriAndKeyring, 1);
  data->uri = g_strdup (uri);
  data->keyring_remote = ostree_remote_ref (keyring_remote);

  return g_steal_pointer (&data);
}

static guint
uri_and_keyring_hash (gconstpointer key)
{
  const UriAndKeyring *_key = key;

  return g_str_hash (_key->uri) ^ g_str_hash (_key->keyring_remote->keyring);
}

static gboolean
uri_and_keyring_equal (gconstpointer a,
                       gconstpointer b)
{
  const UriAndKeyring *_a = a, *_b = b;

  return (g_str_equal (_a->uri, _b->uri) &&
          g_str_equal (_a->keyring_remote->keyring, _b->keyring_remote->keyring));
}

/* This must return a valid remote name (suitable for use in a refspec). */
static gchar *
uri_and_keyring_to_name (UriAndKeyring *data)
{
  g_autofree gchar *escaped_uri = g_uri_escape_string (data->uri, NULL, FALSE);
  g_autofree gchar *escaped_keyring = g_uri_escape_string (data->keyring_remote->keyring, NULL, FALSE);

  /* FIXME: Need a better separator than `_`, since it’s not escaped in the input. */
  g_autofree gchar *out = g_strdup_printf ("%s_%s", escaped_uri, escaped_keyring);

  for (gsize i = 0; out[i] != '\0'; i++)
    {
      if (out[i] == '%')
        out[i] = '_';
    }

  g_return_val_if_fail (ostree_validate_remote_name (out, NULL), NULL);

  return g_steal_pointer (&out);
}

/* Internal structure representing a service found advertised by a peer on the
 * local network. This includes details for connecting to the service, and the
 * metadata associated with the advert (@txt). */
typedef struct
{
  gchar *name;
  gchar *domain;
  gchar *address;
  guint16 port;
  AvahiStringList *txt;
} OstreeAvahiService;

static void
ostree_avahi_service_free (OstreeAvahiService *service)
{
  g_free (service->name);
  g_free (service->domain);
  g_free (service->address);
  avahi_string_list_free (service->txt);
  g_free (service);
}

G_DEFINE_AUTOPTR_CLEANUP_FUNC (OstreeAvahiService, ostree_avahi_service_free)

/* Convert an AvahiAddress to a string which is suitable for use in URIs (for
 * example). Take into account the scope ID, if the address is IPv6 and a
 * link-local address.
 * (See https://en.wikipedia.org/wiki/IPv6_address#Link-local_addresses_and_zone_indices and
 * https://github.com/lathiat/avahi/issues/110.) */
static gchar *
address_to_string (const AvahiAddress *address,
                   AvahiIfIndex        interface)
{
  char address_string[AVAHI_ADDRESS_STR_MAX];

  avahi_address_snprint (address_string, sizeof (address_string), address);

  switch (address->proto)
    {
    case AVAHI_PROTO_INET6:
      if (IN6_IS_ADDR_LINKLOCAL (address->data.data) ||
          IN6_IS_ADDR_LOOPBACK (address->data.data))
        return g_strdup_printf ("%s%%%d", address_string, interface);
      /* else fall through */
    case AVAHI_PROTO_INET:
    case AVAHI_PROTO_UNSPEC:
    default:
      return g_strdup (address_string);
    }
}

static OstreeAvahiService *
ostree_avahi_service_new (const gchar        *name,
                          const gchar        *domain,
                          const AvahiAddress *address,
                          AvahiIfIndex        interface,
                          guint16             port,
                          AvahiStringList    *txt)
{
  g_autoptr(OstreeAvahiService) service = NULL;

  g_return_val_if_fail (name != NULL, NULL);
  g_return_val_if_fail (domain != NULL, NULL);
  g_return_val_if_fail (address != NULL, NULL);
  g_return_val_if_fail (port > 0, NULL);

  service = g_new0 (OstreeAvahiService, 1);

  service->name = g_strdup (name);
  service->domain = g_strdup (domain);
  service->address = address_to_string (address, interface);
  service->port = port;
  service->txt = avahi_string_list_copy (txt);

  return g_steal_pointer (&service);
}

/* Check whether @str is entirely lower case. */
static gboolean
str_is_lowercase (const gchar *str)
{
  gsize i;

  for (i = 0; str[i] != '\0'; i++)
    {
      if (!g_ascii_islower (str[i]))
        return FALSE;
    }

  return TRUE;
}

/* Look up @key in the @attributes table derived from a TXT record, and validate
 * that its value is of type @value_type. If the key is not found, or its value
 * is of the wrong type or is not in normal form, %NULL is returned. @key must
 * be lowercase in order to match reliably. */
static GVariant *
_ostree_txt_records_lookup_variant (GHashTable         *attributes,
                                    const gchar        *key,
                                    const GVariantType *value_type)
{
  GBytes *value;
  g_autoptr(GVariant) variant = NULL;

  g_return_val_if_fail (attributes != NULL, NULL);
  g_return_val_if_fail (str_is_lowercase (key), NULL);
  g_return_val_if_fail (value_type != NULL, NULL);

  value = g_hash_table_lookup (attributes, key);

  if (value == NULL)
    {
      g_debug ("TXT attribute ‘%s’ not found.", key);
      return NULL;
    }

  variant = g_variant_new_from_bytes (value_type, value, FALSE);

  if (!g_variant_is_normal_form (variant))
    {
      g_debug ("TXT attribute ‘%s’ value is not in normal form. Ignoring.", key);
      return NULL;
    }

  return g_steal_pointer (&variant);
}

/* Bloom hash function family for #OstreeCollectionRef, parameterised by @k. */
static guint64
ostree_collection_ref_bloom_hash (gconstpointer element,
                                  guint8        k)
{
  const OstreeCollectionRef *ref = element;

  return ostree_str_bloom_hash (ref->collection_id, k) ^ ostree_str_bloom_hash (ref->ref_name, k);
}

/* Return the (possibly empty) subset of @refs which are possibly in the given
 * encoded bloom filter, @bloom_encoded. The returned array is not
 * %NULL-terminated. If there is an error decoding the bloom filter (invalid
 * type, zero length, unknown hash function), %NULL will be returned. */
static GPtrArray *
bloom_refs_intersection (GVariant                          *bloom_encoded,
                         const OstreeCollectionRef * const *refs)
{
  g_autoptr(OstreeBloom) bloom = NULL;
  g_autoptr(GVariant) bloom_variant = NULL;
  guint8 k, hash_id;
  OstreeBloomHashFunc hash_func;
  const guint8 *bloom_bytes;
  gsize n_bloom_bytes;
  g_autoptr(GBytes) bytes = NULL;
  gsize i;
  g_autoptr(GPtrArray) possible_refs = NULL;  /* (element-type OstreeCollectionRef) */

  g_variant_get (bloom_encoded, "(yy@ay)", &k, &hash_id, &bloom_variant);

  if (k == 0)
    return NULL;

  switch (hash_id)
    {
    case 1:
      hash_func = ostree_collection_ref_bloom_hash;
      break;
    default:
      return NULL;
    }

  bloom_bytes = g_variant_get_fixed_array (bloom_variant, &n_bloom_bytes, sizeof (guint8));
  bytes = g_bytes_new_static (bloom_bytes, n_bloom_bytes);
  bloom = ostree_bloom_new_from_bytes (bytes, k, hash_func);

  possible_refs = g_ptr_array_new_with_free_func (NULL);

  for (i = 0; refs[i] != NULL; i++)
    {
      if (ostree_bloom_maybe_contains (bloom, refs[i]))
        g_ptr_array_add (possible_refs, (gpointer) refs[i]);
    }

  return g_steal_pointer (&possible_refs);
}

/* Given a @summary_map of ref name to commit details, and the @collection_id
 * for all the refs in the @summary_map (which may be %NULL if the summary does
 * not specify one), add the refs to @refs_and_checksums.
 *
 * The @summary_map is validated as it’s iterated over; on error, @error will be
 * set and @refs_and_checksums will be left in an undefined state. */
static gboolean
fill_refs_and_checksums_from_summary_map (GVariantIter  *summary_map,
                                          const gchar   *collection_id,
                                          GHashTable    *refs_and_checksums  /* (element-type OstreeCollectionRef utf8) */,
                                          GError       **error)
{
  g_autofree gchar *ref_name = NULL;
  g_autoptr(GVariant) checksum_variant = NULL;

  while (g_variant_iter_loop (summary_map, "(s(t@aya{sv}))",
                              (gpointer *) &ref_name, NULL,
                              (gpointer *) &checksum_variant, NULL))
    {
      const OstreeCollectionRef ref = { (gchar *) collection_id, ref_name };

      if (!ostree_validate_rev (ref_name, error))
        return FALSE;
      if (!ostree_validate_structureof_csum_v (checksum_variant, error))
        return FALSE;

      if (g_hash_table_contains (refs_and_checksums, &ref))
        {
          g_autofree gchar *checksum_string = ostree_checksum_from_bytes_v (checksum_variant);

          g_hash_table_replace (refs_and_checksums,
                                ostree_collection_ref_dup (&ref),
                                g_steal_pointer (&checksum_string));
        }
    }

  return TRUE;
}

/* Given a @summary file, add the refs it lists to @refs_and_checksums. This
 * includes the main refs list in the summary, and the map of collection IDs
 * to further refs lists.
 *
 * The @summary is validated as it’s explored; on error, @error will be
 * set and @refs_and_checksums will be left in an undefined state. */
static gboolean
fill_refs_and_checksums_from_summary (GVariant    *summary,
                                      GHashTable  *refs_and_checksums  /* (element-type OstreeCollectionRef utf8) */,
                                      GError     **error)
{
  g_autoptr(GVariant) ref_map_v = NULL;
  g_autoptr(GVariant) additional_metadata_v = NULL;
  g_autoptr(GVariantIter) ref_map = NULL;
  g_auto(GVariantDict) additional_metadata = OT_VARIANT_BUILDER_INITIALIZER;
  const gchar *collection_id;
  g_autoptr(GVariantIter) collection_map = NULL;

  ref_map_v = g_variant_get_child_value (summary, 0);
  additional_metadata_v = g_variant_get_child_value (summary, 1);

  ref_map = g_variant_iter_new (ref_map_v);
  g_variant_dict_init (&additional_metadata, additional_metadata_v);

  /* If the summary file specifies a collection ID (to apply to all the refs in its
   * ref map), use that to start matching against the queried refs. Otherwise,
   * it might specify all its refs in a collection-map; or the summary format is
   * old and unsuitable for P2P redistribution and we should bail. */
  if (g_variant_dict_lookup (&additional_metadata, OSTREE_SUMMARY_COLLECTION_ID, "&s", &collection_id))
    {
      if (!ostree_validate_collection_id (collection_id, error))
        return FALSE;
      if (!fill_refs_and_checksums_from_summary_map (ref_map, collection_id, refs_and_checksums, error))
        return FALSE;
    }

  g_clear_pointer (&ref_map, g_variant_iter_free);

  /* Repeat for the other collections listed in the summary. */
  if (g_variant_dict_lookup (&additional_metadata, OSTREE_SUMMARY_COLLECTION_MAP, "a{sa(s(taya{sv}))}", &collection_map))
    {
      while (g_variant_iter_loop (collection_map, "{sa(s(taya{sv}))}", &collection_id, &ref_map))
        {
          if (!ostree_validate_collection_id (collection_id, error))
            return FALSE;
          if (!fill_refs_and_checksums_from_summary_map (ref_map, collection_id, refs_and_checksums, error))
            return FALSE;
        }
    }

  return TRUE;
}

static gboolean
remove_null_checksum_refs_cb (gpointer key,
                              gpointer value,
                              gpointer user_data)
{
  const char *checksum = value;

  return (checksum == NULL);
}

/* Given a summary file (@summary_bytes), extract the refs it lists, and use that
 * to fill in the checksums in the @supported_ref_to_checksum map. This includes
 * the main refs list in the summary, and the map of collection IDs to further
 * refs lists.
 *
 * The @summary is validated as it’s explored; on error, @error will be
 * set and %FALSE will be returned. If the intersection of the summary file refs
 * and the keys in @supported_ref_to_checksum is empty, an error is set. */
static gboolean
get_refs_and_checksums_from_summary (GBytes       *summary_bytes,
                                     GHashTable   *supported_ref_to_checksum /* (element-type OstreeCollectionRef utf8) */,
                                     OstreeRemote *remote,
                                     GError      **error)
{
  g_autoptr(GVariant) summary = g_variant_ref_sink (g_variant_new_from_bytes (OSTREE_SUMMARY_GVARIANT_FORMAT, summary_bytes, FALSE));
  guint removed_refs;

  if (!g_variant_is_normal_form (summary))
    {
      g_set_error_literal (error, G_IO_ERROR, G_IO_ERROR_FAILED,
                           "Not normal form");
      return FALSE;
    }
  if (!g_variant_is_of_type (summary, OSTREE_SUMMARY_GVARIANT_FORMAT))
    {
      g_set_error (error, G_IO_ERROR, G_IO_ERROR_FAILED,
                   "Doesn't match variant type '%s'",
                   (char *)OSTREE_SUMMARY_GVARIANT_FORMAT);
      return FALSE;
    }

  if (!fill_refs_and_checksums_from_summary (summary, supported_ref_to_checksum, error))
    return FALSE;

  removed_refs = g_hash_table_foreach_remove (supported_ref_to_checksum, remove_null_checksum_refs_cb, NULL);
  if (removed_refs > 0)
    g_debug ("Removed %d refs from the list resolved from ‘%s’ (possibly bloom filter false positives)",
             removed_refs, remote->name);

  /* If none of the refs had a non-%NULL checksum set, we can discard this peer. */
  if (g_hash_table_size (supported_ref_to_checksum) == 0)
    {
      g_set_error (error, G_IO_ERROR, G_IO_ERROR_NOT_FOUND,
                   "No matching refs were found in the summary file");
      return FALSE;
    }

  return TRUE;
}

/* Download the summary file from @remote, and return the bytes of the file in
 * @out_summary_bytes. This will return %TRUE and set @out_summary_bytes to %NULL
 * if the summary file does not exist. */
static gboolean
fetch_summary_from_remote (OstreeRepo    *repo,
                           OstreeRemote  *remote,
                           GBytes       **out_summary_bytes,
                           GCancellable  *cancellable,
                           GError       **error)
{
  g_autoptr(GBytes) summary_bytes = NULL;
  gboolean remote_already_existed = _ostree_repo_add_remote (repo, remote);
  gboolean success = ostree_repo_remote_fetch_summary_with_options (repo,
                                                                    remote->name,
                                                                    NULL /* options */,
                                                                    &summary_bytes,
                                                                    NULL /* signature */,
                                                                    cancellable,
                                                                    error);

  if (!remote_already_existed)
    _ostree_repo_remove_remote (repo, remote);

  if (!success)
    return FALSE;

  g_assert (out_summary_bytes != NULL);
  *out_summary_bytes = g_steal_pointer (&summary_bytes);
  return TRUE;
}
#endif  /* HAVE_AVAHI */

struct _OstreeRepoFinderAvahi
{
  GObject parent_instance;

#ifdef HAVE_AVAHI
  /* All elements of this structure must only be accessed from @avahi_context
   * after construction. */

  /* Note: There is a ref-count loop here: each #GTask has a reference to the
   * #OstreeRepoFinderAvahi, and we have to keep a reference to the #GTask. */
  GPtrArray *resolve_tasks;  /* (element-type (owned) GTask) */

  AvahiGLibPoll *poll;
  AvahiClient *client;
  AvahiServiceBrowser *browser;

  AvahiClientState client_state;
  gboolean browser_failed;
  gboolean browser_all_for_now;

  GCancellable *avahi_cancellable;
  GMainContext *avahi_context;

  /* Map of service name (typically human readable) to a #GPtrArray of the
   * #AvahiServiceResolver instances we have running against that name. We
   * could end up with more than one resolver if the same name is advertised to
   * us over multiple interfaces or protocols (for example, IPv4 and IPv6).
   * Resolve all of them just in case one doesn’t work. */
  GHashTable *resolvers;  /* (element-type (owned) utf8 (owned) GPtrArray (element-type (owned) AvahiServiceResolver)) */

  /* Array of #OstreeAvahiService instances representing all the services which
   * we currently think are valid. */
  GPtrArray *found_services;  /* (element-type (owned OstreeAvahiService) */
#endif  /* HAVE_AVAHI */
};

static void ostree_repo_finder_avahi_iface_init (OstreeRepoFinderInterface *iface);

G_DEFINE_TYPE_WITH_CODE (OstreeRepoFinderAvahi, ostree_repo_finder_avahi, G_TYPE_OBJECT,
                         G_IMPLEMENT_INTERFACE (OSTREE_TYPE_REPO_FINDER, ostree_repo_finder_avahi_iface_init))

#ifdef HAVE_AVAHI

/* Download the summary file from @remote and fill in the checksums in the given
 * @supported_ref_to_checksum hash table, given the existing refs in it as keys.
 * See get_refs_and_checksums_from_summary() for more details. */
static gboolean
get_checksums (OstreeRepoFinderAvahi  *finder,
               OstreeRepo             *repo,
               OstreeRemote           *remote,
               GHashTable             *supported_ref_to_checksum /* (element-type OstreeCollectionRef utf8) */,
               GError                **error)
{
  g_autoptr(GBytes) summary_bytes = NULL;

  if (!fetch_summary_from_remote (repo,
                                  remote,
                                  &summary_bytes,
                                  finder->avahi_cancellable,
                                  error))
    return FALSE;

  if (summary_bytes == NULL)
    {
      g_set_error (error, G_IO_ERROR, G_IO_ERROR_NOT_FOUND,
                   "No summary file found on server");
      return FALSE;
    }

  return get_refs_and_checksums_from_summary (summary_bytes, supported_ref_to_checksum, remote, error);
}

/* Build some #OstreeRepoFinderResults out of the given #OstreeAvahiService by
 * parsing its DNS-SD TXT records and finding the intersection between the refs
 * it advertises and @refs. One or more results will be added to @results, with
 * multiple results being added if the intersection of refs covers refs which
 * need different GPG keyrings. One result is added per (uri, keyring) pair.
 *
 * If any of the TXT records are malformed or missing, or if the intersection of
 * refs is empty, return early without modifying @results.
 *
 * This recognises the following TXT records:
 *  - `v` (`y`): Version of the TXT record format. Only version `1` is currently
 *    supported.
 *  - `rb` (`(yyay)`): Bloom filter indicating which refs are served by the peer.
 *  - `st` (`t`): Timestamp (seconds since the Unix epoch, big endian) the
 *    summary file was last modified.
 *  - `ri` (`q`): Repository index, indicating which of several repositories
 *    hosted on the peer this is. Big endian.
 */
static void
ostree_avahi_service_build_repo_finder_result (OstreeAvahiService                *service,
                                               OstreeRepoFinderAvahi             *finder,
                                               OstreeRepo                        *parent_repo,
                                               gint                               priority,
                                               const OstreeCollectionRef * const *refs,
                                               GPtrArray                         *results,
                                               GCancellable                      *cancellable)
{
  g_autoptr(GHashTable) attributes = NULL;
  g_autoptr(GVariant) version = NULL;
  g_autoptr(GVariant) bloom = NULL;
  g_autoptr(GVariant) summary_timestamp = NULL;
  g_autoptr(GVariant) repo_index = NULL;
  g_autofree gchar *repo_path = NULL;
  g_autoptr(GPtrArray) possible_refs = NULL; /* (element-type OstreeCollectionRef) */
  GUri *_uri = NULL;
  g_autofree gchar *uri = NULL;
  g_autoptr(GError) error = NULL;
  gsize i;
  g_autoptr(GHashTable) repo_to_refs = NULL;  /* (element-type UriAndKeyring GHashTable) */
  GHashTable *supported_ref_to_checksum;  /* (element-type OstreeCollectionRef utf8) */
  GHashTableIter iter;
  UriAndKeyring *repo;

  g_return_if_fail (service != NULL);
  g_return_if_fail (refs != NULL);

  attributes = _ostree_txt_records_parse (service->txt);

  /* Check the record version. */
  version = _ostree_txt_records_lookup_variant (attributes, "v", G_VARIANT_TYPE_BYTE);

  if (g_variant_get_byte (version) != 1)
    {
      g_debug ("Unknown v=%02x attribute provided in TXT record. Ignoring.",
               g_variant_get_byte (version));
      return;
    }

  /* Refs bloom filter? */
  bloom = _ostree_txt_records_lookup_variant (attributes, "rb", G_VARIANT_TYPE ("(yyay)"));

  if (bloom == NULL)
    {
      g_debug ("Missing rb (refs bloom) attribute in TXT record. Ignoring.");
      return;
    }

  possible_refs = bloom_refs_intersection (bloom, refs);
  if (possible_refs == NULL)
    {
      g_debug ("Wrong k parameter or hash id in rb (refs bloom) attribute in TXT record. Ignoring.");
      return;
    }
  if (possible_refs->len == 0)
    {
      g_debug ("TXT record definitely has no matching refs. Ignoring.");
      return;
    }

  /* Summary timestamp. */
  summary_timestamp = _ostree_txt_records_lookup_variant (attributes, "st", G_VARIANT_TYPE_UINT64);
  if (summary_timestamp == NULL)
    {
      g_debug ("Missing st (summary timestamp) attribute in TXT record. Ignoring.");
      return;
    }

  /* Repository index. */
  repo_index = _ostree_txt_records_lookup_variant (attributes, "ri", G_VARIANT_TYPE_UINT16);
  if (repo_index == NULL)
    {
      g_debug ("Missing ri (repository index) attribute in TXT record. Ignoring.");
      return;
    }
  repo_path = g_strdup_printf ("/%u", GUINT16_FROM_BE (g_variant_get_uint16 (repo_index)));

  /* Create a new result for each keyring needed by @possible_refs. Typically,
   * there will be a separate keyring per collection, but some might be shared. */
  repo_to_refs = g_hash_table_new_full (uri_and_keyring_hash, uri_and_keyring_equal,
                                        (GDestroyNotify) uri_and_keyring_free, (GDestroyNotify) g_hash_table_unref);

  _uri = g_uri_build (G_URI_FLAGS_ENCODED, "http", NULL, service->address, service->port, repo_path, NULL, NULL);
  uri = g_uri_to_string (_uri);
  g_uri_unref (_uri);

  for (i = 0; i < possible_refs->len; i++)
    {
      const OstreeCollectionRef *ref = g_ptr_array_index (possible_refs, i);
      g_autoptr(UriAndKeyring) resolved_repo = NULL;
      g_autoptr(OstreeRemote) keyring_remote = NULL;

      /* Look up the GPG keyring for this ref. */
      keyring_remote = ostree_repo_resolve_keyring_for_collection (parent_repo,
                                                                   ref->collection_id,
                                                                   cancellable, &error);

      if (keyring_remote == NULL)
        {
          g_debug ("Ignoring ref (%s, %s) on host ‘%s’ due to missing keyring: %s",
                   ref->collection_id, refs[i]->ref_name, service->address,
                   error->message);
          g_clear_error (&error);
          continue;
        }

      /* Add this repo to the results, keyed by the canonicalised repository URI
       * to deduplicate the results. */
      g_debug ("Resolved ref (%s, %s) to repo URI ‘%s’ with keyring ‘%s’ from remote ‘%s’.",
               ref->collection_id, ref->ref_name, uri, keyring_remote->keyring,
               keyring_remote->name);

      resolved_repo = uri_and_keyring_new (uri, keyring_remote);

      supported_ref_to_checksum = g_hash_table_lookup (repo_to_refs, resolved_repo);

      if (supported_ref_to_checksum == NULL)
        {
          supported_ref_to_checksum = g_hash_table_new_full (ostree_collection_ref_hash,
                                                             ostree_collection_ref_equal,
                                                             NULL, g_free);
          g_hash_table_insert (repo_to_refs, g_steal_pointer (&resolved_repo), supported_ref_to_checksum  /* transfer */);
        }

      /* Add a placeholder to @supported_ref_to_checksum for this ref. It will
       * be filled out by the get_checksums() call below. */
      g_hash_table_insert (supported_ref_to_checksum, (gpointer) ref, NULL);
    }

  /* Aggregate the results. */
  g_hash_table_iter_init (&iter, repo_to_refs);

  while (g_hash_table_iter_next (&iter, (gpointer *) &repo, (gpointer *) &supported_ref_to_checksum))
    {
      g_autoptr(OstreeRemote) remote = NULL;

      /* Build an #OstreeRemote. Use the escaped URI, since remote->name
       * is used in file paths, so needs to not contain special characters. */
      g_autofree gchar *name = uri_and_keyring_to_name (repo);
      remote = ostree_remote_new_dynamic (name, repo->keyring_remote->name);

      g_clear_pointer (&remote->keyring, g_free);
      remote->keyring = g_strdup (repo->keyring_remote->keyring);

      /* gpg-verify-summary is false since we use the unsigned summary file support. */
      g_key_file_set_string (remote->options, remote->group, "url", repo->uri);
      g_key_file_set_boolean (remote->options, remote->group, "gpg-verify", TRUE);
      g_key_file_set_boolean (remote->options, remote->group, "gpg-verify-summary", FALSE);

      get_checksums (finder, parent_repo, remote, supported_ref_to_checksum, &error);
      if (error != NULL)
        {
          g_debug ("Failed to get checksums for possible refs; ignoring: %s", error->message);
          g_clear_error (&error);
          continue;
        }

      g_ptr_array_add (results, ostree_repo_finder_result_new (remote, OSTREE_REPO_FINDER (finder),
                                                               priority, supported_ref_to_checksum, NULL,
                                                               GUINT64_FROM_BE (g_variant_get_uint64 (summary_timestamp))));
    }
}

typedef struct
{
  OstreeCollectionRef **refs;  /* (owned) (array zero-terminated=1) */
  OstreeRepo *parent_repo;  /* (owned) */
} ResolveData;

static void
resolve_data_free (ResolveData *data)
{
  g_object_unref (data->parent_repo);
  ostree_collection_ref_freev (data->refs);
  g_free (data);
}

G_DEFINE_AUTOPTR_CLEANUP_FUNC (ResolveData, resolve_data_free)

static ResolveData *
resolve_data_new (const OstreeCollectionRef * const *refs,
                  OstreeRepo                        *parent_repo)
{
  g_autoptr(ResolveData) data = NULL;

  data = g_new0 (ResolveData, 1);
  data->refs = ostree_collection_ref_dupv (refs);
  data->parent_repo = g_object_ref (parent_repo);

  return g_steal_pointer (&data);
}

static void
fail_all_pending_tasks (OstreeRepoFinderAvahi *self,
                        GQuark                 domain,
                        gint                   code,
                        const gchar           *format,
                        ...) G_GNUC_PRINTF(4, 5);

/* Executed in @self->avahi_context.
 *
 * Return the given error from all the pending resolve tasks in
 * self->resolve_tasks. */
static void
fail_all_pending_tasks (OstreeRepoFinderAvahi *self,
                        GQuark                 domain,
                        gint                   code,
                        const gchar           *format,
                        ...)
{
  gsize i;
  va_list args;
  g_autoptr(GError) error = NULL;

  g_assert (g_main_context_is_owner (self->avahi_context));

  va_start (args, format);
  error = g_error_new_valist (domain, code, format, args);
  va_end (args);

  for (i = 0; i < self->resolve_tasks->len; i++)
    {
      GTask *task = G_TASK (g_ptr_array_index (self->resolve_tasks, i));
      g_task_return_error (task, g_error_copy (error));
    }

  g_ptr_array_set_size (self->resolve_tasks, 0);
}

static gint
results_compare_cb (gconstpointer a,
                    gconstpointer b)
{
  const OstreeRepoFinderResult *result_a = *((const OstreeRepoFinderResult **) a);
  const OstreeRepoFinderResult *result_b = *((const OstreeRepoFinderResult **) b);

  return ostree_repo_finder_result_compare (result_a, result_b);
}

/* Executed in @self->avahi_context.
 *
 * For each of the pending resolve tasks in self->resolve_tasks, calculate and
 * return the result set for its query given the currently known services from
 * Avahi which are stored in self->found_services. */
static void
complete_all_pending_tasks (OstreeRepoFinderAvahi *self)
{
  gsize i;
  const gint priority = 60;  /* arbitrarily chosen */
  g_autoptr(GPtrArray) results_for_tasks = g_ptr_array_new_full (self->resolve_tasks->len, (GDestroyNotify)g_ptr_array_unref);
  gboolean cancelled = FALSE;

  g_assert (g_main_context_is_owner (self->avahi_context));
  g_debug ("%s: Completing %u tasks", G_STRFUNC, self->resolve_tasks->len);

  for (i = 0; i < self->resolve_tasks->len; i++)
    {
      g_autoptr(GPtrArray) results = NULL;
      GTask *task;
      ResolveData *data;
      const OstreeCollectionRef * const *refs;
      gsize j;

      task = G_TASK (g_ptr_array_index (self->resolve_tasks, i));
      data = g_task_get_task_data (task);
      refs = (const OstreeCollectionRef * const *) data->refs;
      results = g_ptr_array_new_with_free_func ((GDestroyNotify) ostree_repo_finder_result_free);

      for (j = 0; j < self->found_services->len; j++)
        {
          OstreeAvahiService *service = g_ptr_array_index (self->found_services, j);

          ostree_avahi_service_build_repo_finder_result (service, self, data->parent_repo,
                                                         priority, refs, results,
                                                         self->avahi_cancellable);
          if (g_cancellable_is_cancelled (self->avahi_cancellable))
            {
              cancelled = TRUE;
              break;
            }
        }
      if (cancelled)
        break;

      g_ptr_array_add (results_for_tasks, g_steal_pointer (&results));
    }

  if (!cancelled)
    {
      for (i = 0; i < self->resolve_tasks->len; i++)
        {
          GTask *task = G_TASK (g_ptr_array_index (self->resolve_tasks, i));
          GPtrArray *results = g_ptr_array_index (results_for_tasks, i);

          g_ptr_array_sort (results, results_compare_cb);

          g_task_return_pointer (task,
                                 g_ptr_array_ref (results),
                                 (GDestroyNotify) g_ptr_array_unref);
        }

      g_ptr_array_set_size (self->resolve_tasks, 0);
    }
  else
    {
      fail_all_pending_tasks (self, G_IO_ERROR, G_IO_ERROR_CANCELLED,
                              "Avahi service resolution cancelled.");
    }
}

/* Executed in @self->avahi_context. */
static void
maybe_complete_all_pending_tasks (OstreeRepoFinderAvahi *self)
{
  g_assert (g_main_context_is_owner (self->avahi_context));
  g_debug ("%s: client_state: %s, browser_failed: %u, cancelled: %u, "
           "browser_all_for_now: %u, n_resolvers: %u",
           G_STRFUNC, ostree_avahi_client_state_to_string (self->client_state),
           self->browser_failed,
           g_cancellable_is_cancelled (self->avahi_cancellable),
           self->browser_all_for_now, g_hash_table_size (self->resolvers));

  if (self->client_state == AVAHI_CLIENT_FAILURE)
    fail_all_pending_tasks (self, G_IO_ERROR, G_IO_ERROR_FAILED,
                            "Avahi client error: %s",
                            avahi_strerror (avahi_client_errno (self->client)));
  else if (self->browser_failed)
    fail_all_pending_tasks (self, G_IO_ERROR, G_IO_ERROR_FAILED,
                            "Avahi browser error: %s",
                            avahi_strerror (avahi_client_errno (self->client)));
  else if (g_cancellable_is_cancelled (self->avahi_cancellable))
    fail_all_pending_tasks (self, G_IO_ERROR, G_IO_ERROR_CANCELLED,
                            "Avahi service resolution cancelled.");
  else if (self->browser_all_for_now &&
           g_hash_table_size (self->resolvers) == 0)
    complete_all_pending_tasks (self);
}

/* Executed in @self->avahi_context. */
static void
client_cb (AvahiClient      *client,
           AvahiClientState  state,
           void             *finder_ptr)
{
  /* Completing the pending tasks might drop the final reference to @self. */
  g_autoptr(OstreeRepoFinderAvahi) self = g_object_ref (finder_ptr);

  /* self->client will be NULL if client_cb() is called from
   * ostree_repo_finder_avahi_start(). */
  g_assert (self->client == NULL || g_main_context_is_owner (self->avahi_context));

  g_debug ("%s: Entered state ‘%s’.",
           G_STRFUNC, ostree_avahi_client_state_to_string (state));

  /* We only care about entering and leaving %AVAHI_CLIENT_FAILURE. */
  self->client_state = state;
  if (self->client != NULL)
    maybe_complete_all_pending_tasks (self);
}

/* Executed in @self->avahi_context. */
static void
resolve_cb (AvahiServiceResolver   *resolver,
            AvahiIfIndex            interface,
            AvahiProtocol           protocol,
            AvahiResolverEvent      event,
            const char             *name,
            const char             *type,
            const char             *domain,
            const char             *host_name,
            const AvahiAddress     *address,
            uint16_t                port,
            AvahiStringList        *txt,
            AvahiLookupResultFlags  flags,
            void                   *finder_ptr)
{
  /* Completing the pending tasks might drop the final reference to @self. */
  g_autoptr(OstreeRepoFinderAvahi) self = g_object_ref (finder_ptr);
  g_autoptr(OstreeAvahiService) service = NULL;
  GPtrArray *resolvers;

  g_assert (g_main_context_is_owner (self->avahi_context));

  g_debug ("%s: Resolve event ‘%s’ for name ‘%s’.",
           G_STRFUNC, ostree_avahi_resolver_event_to_string (event), name);

  /* Track the resolvers active for this @name. There may be several,
   * as @name might appear to us over several interfaces or protocols. Most
   * commonly this happens when both hosts are connected via IPv4 and IPv6. */
  resolvers = g_hash_table_lookup (self->resolvers, name);

  if (resolvers == NULL || resolvers->len == 0)
    {
      /* maybe it was removed in the meantime */
      g_hash_table_remove (self->resolvers, name);
      return;
    }
  else if (resolvers->len == 1)
    {
      g_hash_table_remove (self->resolvers, name);
    }
  else
    {
      g_ptr_array_remove_fast (resolvers, resolver);
    }

  /* Was resolution successful? */
  switch (event)
    {
    case AVAHI_RESOLVER_FOUND:
      service = ostree_avahi_service_new (name, domain, address, interface,
                                          port, txt);
      g_ptr_array_add (self->found_services, g_steal_pointer (&service));
      break;
    case AVAHI_RESOLVER_FAILURE:
    default:
      g_warning ("Failed to resolve service ‘%s’: %s", name,
                 avahi_strerror (avahi_client_errno (self->client)));
      break;
    }

  maybe_complete_all_pending_tasks (self);
}

/* Executed in @self->avahi_context. */
static void
browse_new (OstreeRepoFinderAvahi *self,
            AvahiIfIndex           interface,
            AvahiProtocol          protocol,
            const gchar           *name,
            const gchar           *type,
            const gchar           *domain)
{
  g_autoptr(AvahiServiceResolver) resolver = NULL;
  GPtrArray *resolvers;  /* (element-type AvahiServiceResolver) */

  g_assert (g_main_context_is_owner (self->avahi_context));

  resolver = avahi_service_resolver_new (self->client,
                                         interface,
                                         protocol,
                                         name,
                                         type,
                                         domain,
                                         AVAHI_PROTO_UNSPEC,
                                         0,
                                         resolve_cb,
                                         self);
  if (resolver == NULL)
    {
      g_warning ("Failed to resolve service ‘%s’: %s", name,
                 avahi_strerror (avahi_client_errno (self->client)));
      return;
    }

  g_debug ("Found name service %s on the network; type: %s, domain: %s, "
           "protocol: %u, interface: %u", name, type, domain, protocol,
           interface);

  /* Start a resolver for this (interface, protocol, name, type, domain)
   * combination. */
  resolvers = g_hash_table_lookup (self->resolvers, name);
  if (resolvers == NULL)
    {
      resolvers = g_ptr_array_new_with_free_func ((GDestroyNotify) avahi_service_resolver_free);
      g_hash_table_insert (self->resolvers, g_strdup (name), resolvers);
    }

  g_ptr_array_add (resolvers, g_steal_pointer (&resolver));
}

/* Executed in @self->avahi_context. Caller must call maybe_complete_all_pending_tasks(). */
static void
browse_remove (OstreeRepoFinderAvahi *self,
               const char            *name)
{
  gsize i;
  gboolean removed = FALSE;

  g_assert (g_main_context_is_owner (self->avahi_context));

  g_hash_table_remove (self->resolvers, name);

  for (i = 0; i < self->found_services->len; i += (removed ? 0 : 1))
    {
      OstreeAvahiService *service = g_ptr_array_index (self->found_services, i);

      removed = FALSE;

      if (g_strcmp0 (service->name, name) == 0)
        {
          g_ptr_array_remove_index_fast (self->found_services, i);
          removed = TRUE;
          continue;
        }
    }
}

/* Executed in @self->avahi_context. */
static void
browse_cb (AvahiServiceBrowser    *browser,
           AvahiIfIndex            interface,
           AvahiProtocol           protocol,
           AvahiBrowserEvent       event,
           const char             *name,
           const char             *type,
           const char             *domain,
           AvahiLookupResultFlags  flags,
           void                   *finder_ptr)
{
  /* Completing the pending tasks might drop the final reference to @self. */
  g_autoptr(OstreeRepoFinderAvahi) self = g_object_ref (finder_ptr);

  g_assert (g_main_context_is_owner (self->avahi_context));

  g_debug ("%s: Browse event ‘%s’ for name ‘%s’.",
           G_STRFUNC, ostree_avahi_browser_event_to_string (event), name);

  self->browser_failed = FALSE;

  switch (event)
    {
    case AVAHI_BROWSER_NEW:
      browse_new (self, interface, protocol, name, type, domain);
      break;

    case AVAHI_BROWSER_REMOVE:
      browse_remove (self, name);
      break;

    case AVAHI_BROWSER_CACHE_EXHAUSTED:
      /* don’t care about this. */
      break;

    case AVAHI_BROWSER_ALL_FOR_NOW:
      self->browser_all_for_now = TRUE;
      break;

    case AVAHI_BROWSER_FAILURE:
      self->browser_failed = TRUE;
      break;

    default:
      g_assert_not_reached ();
    }

  /* Check all the tasks for any event, since the @browser_failed state
   * may have changed. */
  maybe_complete_all_pending_tasks (self);
}

static gboolean add_resolve_task_cb (gpointer user_data);
#endif  /* HAVE_AVAHI */

static void
ostree_repo_finder_avahi_resolve_async (OstreeRepoFinder                  *finder,
                                        const OstreeCollectionRef * const *refs,
                                        OstreeRepo                        *parent_repo,
                                        GCancellable                      *cancellable,
                                        GAsyncReadyCallback                callback,
                                        gpointer                           user_data)
{
  OstreeRepoFinderAvahi *self = OSTREE_REPO_FINDER_AVAHI (finder);
  g_autoptr(GTask) task = NULL;

  g_debug ("%s: Starting resolving", G_STRFUNC);

  task = g_task_new (self, cancellable, callback, user_data);
  g_task_set_source_tag (task, ostree_repo_finder_avahi_resolve_async);

#ifdef HAVE_AVAHI
  g_task_set_task_data (task, resolve_data_new (refs, parent_repo), (GDestroyNotify) resolve_data_free);

  /* Move @task to the @avahi_context where it can be processed. */
  g_main_context_invoke (self->avahi_context, add_resolve_task_cb, g_steal_pointer (&task));
#else  /* if !HAVE_AVAHI */
  g_task_return_new_error (task, G_IO_ERROR, G_IO_ERROR_NOT_SUPPORTED,
                           "Avahi support was not compiled in to libostree");
#endif  /* !HAVE_AVAHI */
}

#ifdef HAVE_AVAHI
/* Executed in @self->avahi_context. */
static gboolean
add_resolve_task_cb (gpointer user_data)
{
  g_autoptr(GTask) task = G_TASK (user_data);
  OstreeRepoFinderAvahi *self = g_task_get_source_object (task);

  g_assert (g_main_context_is_owner (self->avahi_context));
  g_debug ("%s", G_STRFUNC);

  /* Track the task and check to see if the browser and resolvers are in a
   * quiescent state suitable for returning a result immediately. */
  g_ptr_array_add (self->resolve_tasks, g_object_ref (task));
  maybe_complete_all_pending_tasks (self);

  return G_SOURCE_REMOVE;
}
#endif  /* HAVE_AVAHI */

static GPtrArray *
ostree_repo_finder_avahi_resolve_finish (OstreeRepoFinder  *finder,
                                         GAsyncResult      *result,
                                         GError           **error)
{
  g_return_val_if_fail (g_task_is_valid (result, finder), NULL);
  return g_task_propagate_pointer (G_TASK (result), error);
}

static void
ostree_repo_finder_avahi_dispose (GObject *obj)
{
#ifdef HAVE_AVAHI
  OstreeRepoFinderAvahi *self = OSTREE_REPO_FINDER_AVAHI (obj);

  ostree_repo_finder_avahi_stop (self);

  g_assert (self->resolve_tasks == NULL || self->resolve_tasks->len == 0);

  g_clear_pointer (&self->resolve_tasks, g_ptr_array_unref);
  g_clear_pointer (&self->browser, avahi_service_browser_free);
  g_clear_pointer (&self->client, avahi_client_free);
  g_clear_pointer (&self->poll, avahi_glib_poll_free);
  g_clear_pointer (&self->avahi_context, g_main_context_unref);
  g_clear_pointer (&self->found_services, g_ptr_array_unref);
  g_clear_pointer (&self->resolvers, g_hash_table_unref);
  g_clear_object (&self->avahi_cancellable);
#endif  /* HAVE_AVAHI */

  /* Chain up. */
  G_OBJECT_CLASS (ostree_repo_finder_avahi_parent_class)->dispose (obj);
}

static void
ostree_repo_finder_avahi_class_init (OstreeRepoFinderAvahiClass *klass)
{
  GObjectClass *object_class = G_OBJECT_CLASS (klass);

  object_class->dispose = ostree_repo_finder_avahi_dispose;
}

static void
ostree_repo_finder_avahi_iface_init (OstreeRepoFinderInterface *iface)
{
  iface->resolve_async = ostree_repo_finder_avahi_resolve_async;
  iface->resolve_finish = ostree_repo_finder_avahi_resolve_finish;
}

static void
ostree_repo_finder_avahi_init (OstreeRepoFinderAvahi *self)
{
#ifdef HAVE_AVAHI
  self->resolve_tasks = g_ptr_array_new_with_free_func ((GDestroyNotify) g_object_unref);
  self->avahi_cancellable = g_cancellable_new ();
  self->client_state = AVAHI_CLIENT_S_REGISTERING;
  self->resolvers = g_hash_table_new_full (g_str_hash, g_str_equal, g_free, (GDestroyNotify) g_ptr_array_unref);
  self->found_services = g_ptr_array_new_with_free_func ((GDestroyNotify) ostree_avahi_service_free);
#endif  /* HAVE_AVAHI */
}

/**
 * ostree_repo_finder_avahi_new:
 * @context: (transfer none) (nullable): a #GMainContext for processing Avahi
 *    events in, or %NULL to use the current thread-default
 *
 * Create a new #OstreeRepoFinderAvahi instance. It is intended that one such
 * instance be created per process, and it be used to answer all resolution
 * requests from #OstreeRepos.
 *
 * The calling code is responsible for ensuring that @context is iterated while
 * the #OstreeRepoFinderAvahi is running (after ostree_repo_finder_avahi_start()
 * is called). This may be done from any thread.
 *
 * If @context is %NULL, the current thread-default #GMainContext is used.
 *
 * Returns: (transfer full): a new #OstreeRepoFinderAvahi
 * Since: 2018.6
 */
OstreeRepoFinderAvahi *
ostree_repo_finder_avahi_new (GMainContext *context)
{
  g_autoptr(OstreeRepoFinderAvahi) finder = NULL;

  finder = g_object_new (OSTREE_TYPE_REPO_FINDER_AVAHI, NULL);

#ifdef HAVE_AVAHI
  /* FIXME: Make this a property */
  if (context != NULL)
    finder->avahi_context = g_main_context_ref (context);
  else
    finder->avahi_context = g_main_context_ref_thread_default ();

  /* Avahi setup. Note: Technically the allocator is per-process state which we
   * shouldn’t set here, but it’s probably fine. It’s unlikely that code which
   * is using libostree is going to use an allocator which is not GLib, and
   * *also* use Avahi API itself. */
  avahi_set_allocator (avahi_glib_allocator ());
  finder->poll = avahi_glib_poll_new (finder->avahi_context, G_PRIORITY_DEFAULT);
#endif  /* HAVE_AVAHI */

  return g_steal_pointer (&finder);
}

/**
 * ostree_repo_finder_avahi_start:
 * @self: an #OstreeRepoFinderAvahi
 * @error: return location for a #GError
 *
 * Start monitoring the local network for peers who are advertising OSTree
 * repositories, using Avahi. In order for this to work, the #GMainContext
 * passed to @self at construction time must be iterated (so it will typically
 * be the global #GMainContext, or be a separate #GMainContext in a worker
 * thread).
 *
 * This will return an error (%G_IO_ERROR_FAILED) if initialisation fails, or if
 * Avahi support is not available (%G_IO_ERROR_NOT_SUPPORTED). In either case,
 * the #OstreeRepoFinderAvahi instance is useless afterwards and should be
 * destroyed.
 *
 * Call ostree_repo_finder_avahi_stop() to stop the repo finder.
 *
 * It is an error to call this function multiple times on the same
 * #OstreeRepoFinderAvahi instance, or to call it after
 * ostree_repo_finder_avahi_stop().
 *
 * Since: 2018.6
 */
void
ostree_repo_finder_avahi_start (OstreeRepoFinderAvahi  *self,
                                GError                **error)
{
  g_return_if_fail (OSTREE_IS_REPO_FINDER_AVAHI (self));
  g_return_if_fail (error == NULL || *error == NULL);

#ifdef HAVE_AVAHI
  g_autoptr(AvahiClient) client = NULL;
  g_autoptr(AvahiServiceBrowser) browser = NULL;
  int failure = 0;

  if (g_cancellable_set_error_if_cancelled (self->avahi_cancellable, error))
    return;

  g_assert (self->client == NULL);

  client = avahi_client_new (avahi_glib_poll_get (self->poll), 0,
                             client_cb, self, &failure);

  if (client == NULL)
    {
      if (failure == AVAHI_ERR_NO_DAEMON)
        g_set_error (error, G_IO_ERROR, G_IO_ERROR_NOT_FOUND,
                     "Avahi daemon is not running: %s",
                     avahi_strerror (failure));
      else
        g_set_error (error, G_IO_ERROR, G_IO_ERROR_FAILED,
                     "Failed to create finder client: %s",
                     avahi_strerror (failure));

      return;
    }

  /* Query for the OSTree DNS-SD service on the local network. */
  browser = avahi_service_browser_new (client,
                                       AVAHI_IF_UNSPEC,
                                       AVAHI_PROTO_UNSPEC,
                                       OSTREE_AVAHI_SERVICE_TYPE,
                                       NULL,
                                       0,
                                       browse_cb,
                                       self);

  if (browser == NULL)
    {
      g_set_error (error, G_IO_ERROR, G_IO_ERROR_FAILED,
                   "Failed to create service browser: %s",
                   avahi_strerror (avahi_client_errno (client)));
      return;
    }

  /* Success. */
  self->client = g_steal_pointer (&client);
  self->browser = g_steal_pointer (&browser);
#else  /* if !HAVE_AVAHI */
  g_set_error (error, G_IO_ERROR, G_IO_ERROR_NOT_SUPPORTED,
               "Avahi support was not compiled in to libostree");
#endif  /* !HAVE_AVAHI */
}

#ifdef HAVE_AVAHI
static gboolean stop_cb (gpointer user_data);
#endif  /* HAVE_AVAHI */

/**
 * ostree_repo_finder_avahi_stop:
 * @self: an #OstreeRepoFinderAvahi
 *
 * Stop monitoring the local network for peers who are advertising OSTree
 * repositories. If any resolve tasks (from ostree_repo_finder_resolve_async())
 * are in progress, they will be cancelled and will return %G_IO_ERROR_CANCELLED.
 *
 * Call ostree_repo_finder_avahi_start() to start the repo finder.
 *
 * It is an error to call this function multiple times on the same
 * #OstreeRepoFinderAvahi instance, or to call it before
 * ostree_repo_finder_avahi_start().
 *
 * Since: 2018.6
 */
void
ostree_repo_finder_avahi_stop (OstreeRepoFinderAvahi *self)
{
  g_return_if_fail (OSTREE_IS_REPO_FINDER_AVAHI (self));

#ifdef HAVE_AVAHI
  if (self->browser == NULL)
    return;

  g_main_context_invoke (self->avahi_context, stop_cb, g_object_ref (self));
#endif  /* HAVE_AVAHI */
}

#ifdef HAVE_AVAHI
static gboolean
stop_cb (gpointer user_data)
{
  g_autoptr(OstreeRepoFinderAvahi) self = OSTREE_REPO_FINDER_AVAHI (user_data);

  g_cancellable_cancel (self->avahi_cancellable);
  maybe_complete_all_pending_tasks (self);

  g_clear_pointer (&self->browser, avahi_service_browser_free);
  g_clear_pointer (&self->client, avahi_client_free);
  g_hash_table_remove_all (self->resolvers);

  return G_SOURCE_REMOVE;
}
#endif  /* HAVE_AVAHI */
