diff --git a/Makefile-libostree-defines.am b/Makefile-libostree-defines.am
index 7586f7b9..6fc4a18a 100644
--- a/Makefile-libostree-defines.am
+++ b/Makefile-libostree-defines.am
@@ -43,6 +43,7 @@ libostree_public_headers += \
src/libostree/ostree-ref.h \
src/libostree/ostree-remote.h \
src/libostree/ostree-repo-finder.h \
+ src/libostree/ostree-repo-finder-avahi.h \
src/libostree/ostree-repo-finder-config.h \
src/libostree/ostree-repo-finder-mount.h \
$(NULL)
diff --git a/Makefile-libostree.am b/Makefile-libostree.am
index 6a7c4820..8ff89e52 100644
--- a/Makefile-libostree.am
+++ b/Makefile-libostree.am
@@ -155,6 +155,7 @@ libostree_1_la_SOURCES += \
src/libostree/ostree-ref.h \
src/libostree/ostree-remote.h \
src/libostree/ostree-repo-finder.h \
+ src/libostree/ostree-repo-finder-avahi.h \
src/libostree/ostree-repo-finder-config.h \
src/libostree/ostree-repo-finder-mount.h \
$(NULL)
@@ -163,9 +164,17 @@ libostree_1_la_SOURCES += \
src/libostree/ostree-bloom.c \
src/libostree/ostree-bloom-private.h \
src/libostree/ostree-repo-finder.c \
+ src/libostree/ostree-repo-finder-avahi.c \
src/libostree/ostree-repo-finder-config.c \
src/libostree/ostree-repo-finder-mount.c \
$(NULL)
+
+if USE_AVAHI
+libostree_1_la_SOURCES += \
+ src/libostree/ostree-repo-finder-avahi-parser.c \
+ src/libostree/ostree-repo-finder-avahi-private.h \
+ $(NULL)
+endif # USE_AVAHI
endif
symbol_files = $(top_srcdir)/src/libostree/libostree-released.sym
@@ -193,6 +202,13 @@ libostree_1_la_CFLAGS += $(OT_DEP_LIBARCHIVE_CFLAGS)
libostree_1_la_LIBADD += $(OT_DEP_LIBARCHIVE_LIBS)
endif
+if ENABLE_EXPERIMENTAL_API
+if USE_AVAHI
+libostree_1_la_CFLAGS += $(OT_DEP_AVAHI_CFLAGS)
+libostree_1_la_LIBADD += $(OT_DEP_AVAHI_LIBS)
+endif
+endif
+
if BUILDOPT_LIBSYSTEMD
libostree_1_la_CFLAGS += $(LIBSYSTEMD_CFLAGS)
libostree_1_la_LIBADD += $(LIBSYSTEMD_LIBS)
@@ -241,7 +257,7 @@ OSTree_1_0_gir_INCLUDES = Gio-2.0
OSTree_1_0_gir_CFLAGS = $(libostree_1_la_CFLAGS)
OSTree_1_0_gir_LIBS = libostree-1.la
OSTree_1_0_gir_SCANNERFLAGS = --warn-all --identifier-prefix=Ostree --symbol-prefix=ostree
-OSTree_1_0_gir_FILES = $(libostreeinclude_HEADERS) $(filter-out %-private.h %/ostree-soup-uri.h %/ostree-repo-finder.h %/ostree-repo-finder-config.h %/ostree-repo-finder-mount.h,$(libostree_1_la_SOURCES))
+OSTree_1_0_gir_FILES = $(libostreeinclude_HEADERS) $(filter-out %-private.h %/ostree-soup-uri.h %/ostree-repo-finder.h %/ostree-repo-finder-avahi.h %/ostree-repo-finder-config.h %/ostree-repo-finder-mount.h,$(libostree_1_la_SOURCES))
INTROSPECTION_GIRS += OSTree-1.0.gir
gir_DATA += OSTree-1.0.gir
typelib_DATA += OSTree-1.0.typelib
diff --git a/Makefile-tests.am b/Makefile-tests.am
index d04a1cbc..c21e29f7 100644
--- a/Makefile-tests.am
+++ b/Makefile-tests.am
@@ -199,6 +199,10 @@ test_programs += \
tests/test-repo-finder-config \
tests/test-repo-finder-mount \
$(NULL)
+
+if USE_AVAHI
+test_programs += tests/test-repo-finder-avahi
+endif
endif
# An interactive tool
@@ -231,6 +235,12 @@ tests_test_bloom_SOURCES = src/libostree/ostree-bloom.c tests/test-bloom.c
tests_test_bloom_CFLAGS = $(TESTS_CFLAGS)
tests_test_bloom_LDADD = $(TESTS_LDADD)
+if USE_AVAHI
+tests_test_repo_finder_avahi_SOURCES = src/libostree/ostree-repo-finder-avahi-parser.c tests/test-repo-finder-avahi.c
+tests_test_repo_finder_avahi_CFLAGS = $(TESTS_CFLAGS)
+tests_test_repo_finder_avahi_LDADD = $(TESTS_LDADD)
+endif
+
tests_test_repo_finder_config_SOURCES = tests/test-repo-finder-config.c
tests_test_repo_finder_config_CFLAGS = $(TESTS_CFLAGS)
tests_test_repo_finder_config_LDADD = $(TESTS_LDADD)
diff --git a/apidoc/ostree-experimental-sections.txt b/apidoc/ostree-experimental-sections.txt
index c977b56c..4c71fad2 100644
--- a/apidoc/ostree-experimental-sections.txt
+++ b/apidoc/ostree-experimental-sections.txt
@@ -49,6 +49,16 @@ ostree_repo_finder_get_type
ostree_repo_finder_result_get_type
+
+ostree-repo-finder-avahi
+OstreeRepoFinderAvahi
+ostree_repo_finder_avahi_new
+ostree_repo_finder_avahi_start
+ostree_repo_finder_avahi_stop
+
+ostree_repo_finder_avahi_get_type
+
+
ostree-repo-finder-config
OstreeRepoFinderConfig
diff --git a/configure.ac b/configure.ac
index 9973c80a..67e70b3e 100644
--- a/configure.ac
+++ b/configure.ac
@@ -320,6 +320,31 @@ if test x$with_openssl != xno; then OSTREE_FEATURES="$OSTREE_FEATURES openssl";
AM_CONDITIONAL(USE_OPENSSL, test $with_openssl != no)
dnl end openssl
+dnl Avahi dependency for finding repos
+AVAHI_DEPENDENCY="avahi-client >= 0.6.31 avahi-glib >= 0.6.31"
+
+AC_ARG_WITH(avahi,
+ AS_HELP_STRING([--without-avahi], [Do not use Avahi]),
+ :, with_avahi=maybe)
+
+AS_IF([ test x$with_avahi != xno ], [
+ AC_MSG_CHECKING([for $AVAHI_DEPENDENCY])
+ PKG_CHECK_EXISTS($AVAHI_DEPENDENCY, have_avahi=yes, have_avahi=no)
+ AC_MSG_RESULT([$have_avahi])
+ AS_IF([ test x$have_avahi = xno && test x$with_avahi != xmaybe ], [
+ AC_MSG_ERROR([Avahi is enabled but could not be found])
+ ])
+ AS_IF([ test x$have_avahi = xyes], [
+ AC_DEFINE([HAVE_AVAHI], 1, [Define if we have avahi-client.pc and avahi-glib.pc])
+ PKG_CHECK_MODULES(OT_DEP_AVAHI, $AVAHI_DEPENDENCY)
+ with_avahi=yes
+ ], [
+ with_avahi=no
+ ])
+], [ with_avahi=no ])
+if test x$with_avahi != xno; then OSTREE_FEATURES="$OSTREE_FEATURES avahi"; fi
+AM_CONDITIONAL(USE_AVAHI, test $with_avahi != no)
+
dnl This is what is in RHEL7.2 right now, picking it arbitrarily
LIBMOUNT_DEPENDENCY="mount >= 2.23.0"
diff --git a/src/libostree/libostree-experimental.sym b/src/libostree/libostree-experimental.sym
index e70c80bb..32ba0929 100644
--- a/src/libostree/libostree-experimental.sym
+++ b/src/libostree/libostree-experimental.sym
@@ -47,6 +47,10 @@ global:
ostree_collection_ref_new;
ostree_repo_find_remotes_async;
ostree_repo_find_remotes_finish;
+ ostree_repo_finder_avahi_get_type;
+ ostree_repo_finder_avahi_new;
+ ostree_repo_finder_avahi_start;
+ ostree_repo_finder_avahi_stop;
ostree_repo_finder_config_get_type;
ostree_repo_finder_config_new;
ostree_repo_finder_get_type;
diff --git a/src/libostree/ostree-autocleanups.h b/src/libostree/ostree-autocleanups.h
index dd3c9778..b375413c 100644
--- a/src/libostree/ostree-autocleanups.h
+++ b/src/libostree/ostree-autocleanups.h
@@ -64,6 +64,7 @@ G_DEFINE_AUTOPTR_CLEANUP_FUNC (OstreeCollectionRef, ostree_collection_ref_free)
G_DEFINE_AUTO_CLEANUP_FREE_FUNC (OstreeCollectionRefv, ostree_collection_ref_freev, NULL)
G_DEFINE_AUTOPTR_CLEANUP_FUNC (OstreeRemote, ostree_remote_unref)
G_DEFINE_AUTOPTR_CLEANUP_FUNC (OstreeRepoFinder, g_object_unref)
+G_DEFINE_AUTOPTR_CLEANUP_FUNC (OstreeRepoFinderAvahi, g_object_unref)
G_DEFINE_AUTOPTR_CLEANUP_FUNC (OstreeRepoFinderConfig, g_object_unref)
G_DEFINE_AUTOPTR_CLEANUP_FUNC (OstreeRepoFinderMount, g_object_unref)
G_DEFINE_AUTOPTR_CLEANUP_FUNC (OstreeRepoFinderResult, ostree_repo_finder_result_free)
diff --git a/src/libostree/ostree-core-private.h b/src/libostree/ostree-core-private.h
index a56fdc0b..76c76cc7 100644
--- a/src/libostree/ostree-core-private.h
+++ b/src/libostree/ostree-core-private.h
@@ -188,6 +188,9 @@ G_DEFINE_AUTOPTR_CLEANUP_FUNC (OstreeRepoFinder, g_object_unref)
G_DEFINE_AUTOPTR_CLEANUP_FUNC (OstreeRepoFinderResult, ostree_repo_finder_result_free)
G_DEFINE_AUTO_CLEANUP_FREE_FUNC (OstreeRepoFinderResultv, ostree_repo_finder_result_freev, NULL)
+#include "ostree-repo-finder-avahi.h"
+G_DEFINE_AUTOPTR_CLEANUP_FUNC (OstreeRepoFinderAvahi, g_object_unref)
+
#include "ostree-repo-finder-config.h"
G_DEFINE_AUTOPTR_CLEANUP_FUNC (OstreeRepoFinderConfig, g_object_unref)
diff --git a/src/libostree/ostree-repo-finder-avahi-parser.c b/src/libostree/ostree-repo-finder-avahi-parser.c
new file mode 100644
index 00000000..805f5dff
--- /dev/null
+++ b/src/libostree/ostree-repo-finder-avahi-parser.c
@@ -0,0 +1,137 @@
+/* -*- mode: C; c-file-style: "gnu"; indent-tabs-mode: nil; -*-
+ *
+ * Copyright © 2016 Kinvolk GmbH
+ * Copyright © 2017 Endless Mobile, Inc.
+ *
+ * This library is free software; you can redistribute it and/or
+ * modify it under the terms of the GNU Lesser General Public
+ * License as published by the Free Software Foundation; either
+ * version 2 of the License, or (at your option) any later version.
+ *
+ * This library is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
+ * Lesser General Public License for more details.
+ *
+ * You should have received a copy of the GNU Lesser General Public
+ * License along with this library; if not, write to the
+ * Free Software Foundation, Inc., 59 Temple Place - Suite 330,
+ * Boston, MA 02111-1307, USA.
+ *
+ * Authors:
+ * - Krzesimir Nowak
+ * - Philip Withnall
+ */
+
+#include "config.h"
+
+#include
+#include
+#include
+#include
+
+#include "ostree-autocleanups.h"
+#include "ostree-repo-finder-avahi.h"
+#include "ostree-repo-finder-avahi-private.h"
+
+/* Reference: RFC 6763, §6. */
+static gboolean
+parse_txt_record (const guint8 *txt,
+ gsize txt_len,
+ const gchar **key,
+ gsize *key_len,
+ const guint8 **value,
+ gsize *value_len)
+{
+ gsize i;
+
+ g_return_val_if_fail (key != NULL, FALSE);
+ g_return_val_if_fail (key_len != NULL, FALSE);
+ g_return_val_if_fail (value != NULL, FALSE);
+ g_return_val_if_fail (value_len != NULL, FALSE);
+
+ /* RFC 6763, §6.1. */
+ if (txt_len > 8900)
+ return FALSE;
+
+ *key = (const gchar *) txt;
+ *key_len = 0;
+ *value = NULL;
+ *value_len = 0;
+
+ for (i = 0; i < txt_len; i++)
+ {
+ if (txt[i] >= 0x20 && txt[i] <= 0x7e && txt[i] != '=')
+ {
+ /* Key character. */
+ *key_len = *key_len + 1;
+ continue;
+ }
+ else if (*key_len > 0 && txt[i] == '=')
+ {
+ /* Separator. */
+ *value = txt + (i + 1);
+ *value_len = txt_len - (i + 1);
+ return TRUE;
+ }
+ else
+ {
+ return FALSE;
+ }
+ }
+
+ /* The entire TXT record is the key; there is no ‘=’ or value. */
+ *value = NULL;
+ *value_len = 0;
+
+ return (*key_len > 0);
+}
+
+/* TODO: Docs. Return value is only valid as long as @txt is. Reference: RFC 6763, §6. */
+GHashTable *
+_ostree_txt_records_parse (AvahiStringList *txt)
+{
+ AvahiStringList *l;
+ g_autoptr(GHashTable) out = NULL;
+
+ out = g_hash_table_new_full (g_str_hash, g_str_equal, g_free, (GDestroyNotify) g_bytes_unref);
+
+ for (l = txt; l != NULL; l = avahi_string_list_get_next (l))
+ {
+ const guint8 *txt;
+ gsize txt_len;
+ const gchar *key;
+ const guint8 *value;
+ gsize key_len, value_len;
+ g_autofree gchar *key_allocated = NULL;
+ g_autoptr(GBytes) value_allocated = NULL;
+
+ txt = avahi_string_list_get_text (l);
+ txt_len = avahi_string_list_get_size (l);
+
+ if (!parse_txt_record (txt, txt_len, &key, &key_len, &value, &value_len))
+ {
+ g_debug ("Ignoring invalid TXT record of length %" G_GSIZE_FORMAT,
+ txt_len);
+ continue;
+ }
+
+ key_allocated = g_ascii_strdown (key, key_len);
+
+ if (g_hash_table_lookup_extended (out, key_allocated, NULL, NULL))
+ {
+ g_debug ("Ignoring duplicate TXT record ‘%s’", key_allocated);
+ continue;
+ }
+
+ /* Distinguish between the case where the entire record is the key
+ * (value == NULL) and the case where the record is the key + ‘=’ and the
+ * value is empty (value != NULL && value_len == 0). */
+ if (value != NULL)
+ value_allocated = g_bytes_new_static (value, value_len);
+
+ g_hash_table_insert (out, g_steal_pointer (&key_allocated), g_steal_pointer (&value_allocated));
+ }
+
+ return g_steal_pointer (&out);
+}
diff --git a/src/libostree/ostree-repo-finder-avahi-private.h b/src/libostree/ostree-repo-finder-avahi-private.h
new file mode 100644
index 00000000..6429cd7f
--- /dev/null
+++ b/src/libostree/ostree-repo-finder-avahi-private.h
@@ -0,0 +1,35 @@
+/* -*- mode: C; c-file-style: "gnu"; indent-tabs-mode: nil; -*-
+ *
+ * Copyright © 2017 Endless Mobile, Inc.
+ *
+ * This library is free software; you can redistribute it and/or
+ * modify it under the terms of the GNU Lesser General Public
+ * License as published by the Free Software Foundation; either
+ * version 2 of the License, or (at your option) any later version.
+ *
+ * This library is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
+ * Lesser General Public License for more details.
+ *
+ * You should have received a copy of the GNU Lesser General Public
+ * License along with this library; if not, write to the
+ * Free Software Foundation, Inc., 59 Temple Place - Suite 330,
+ * Boston, MA 02111-1307, USA.
+ *
+ * Authors:
+ * - Philip Withnall
+ */
+
+#pragma once
+
+#include
+#include
+#include
+#include
+
+G_BEGIN_DECLS
+
+GHashTable *_ostree_txt_records_parse (AvahiStringList *txt);
+
+G_END_DECLS
diff --git a/src/libostree/ostree-repo-finder-avahi.c b/src/libostree/ostree-repo-finder-avahi.c
new file mode 100644
index 00000000..433914b4
--- /dev/null
+++ b/src/libostree/ostree-repo-finder-avahi.c
@@ -0,0 +1,1506 @@
+/* -*- mode: C; c-file-style: "gnu"; indent-tabs-mode: nil; -*-
+ *
+ * Copyright © 2016 Kinvolk GmbH
+ * Copyright © 2017 Endless Mobile, Inc.
+ *
+ * This library is free software; you can redistribute it and/or
+ * modify it under the terms of the GNU Lesser General Public
+ * License as published by the Free Software Foundation; either
+ * version 2 of the License, or (at your option) any later version.
+ *
+ * This library is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
+ * Lesser General Public License for more details.
+ *
+ * You should have received a copy of the GNU Lesser General Public
+ * License along with this library; if not, write to the
+ * Free Software Foundation, Inc., 59 Temple Place - Suite 330,
+ * Boston, MA 02111-1307, USA.
+ *
+ * Authors:
+ * - Krzesimir Nowak
+ * - Philip Withnall
+ */
+
+#include "config.h"
+
+#ifdef HAVE_AVAHI
+#include
+#include
+#include
+#include
+#include
+#include
+#include
+#include
+#include
+#include
+#include
+#include
+#endif /* HAVE_AVAHI */
+
+#include
+#include
+#include
+
+#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: 2017.8
+ */
+
+#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;
+ gchar *keyring;
+} UriAndKeyring;
+
+static void
+uri_and_keyring_free (UriAndKeyring *data)
+{
+ g_free (data->uri);
+ g_free (data->keyring);
+ g_free (data);
+}
+
+G_DEFINE_AUTOPTR_CLEANUP_FUNC (UriAndKeyring, uri_and_keyring_free)
+
+static UriAndKeyring *
+uri_and_keyring_new (const gchar *uri,
+ const gchar *keyring)
+{
+ g_autoptr(UriAndKeyring) data = NULL;
+
+ data = g_new0 (UriAndKeyring, 1);
+ data->uri = g_strdup (uri);
+ data->keyring = g_strdup (keyring);
+
+ 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);
+}
+
+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, _b->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, 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_next (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;
+ GVariantIter ref_map;
+ 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);
+
+ g_variant_iter_init (&ref_map, 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;
+ }
+
+ /* 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;
+}
+
+/* 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) */,
+ GError **error)
+{
+ g_autoptr(GVariant) summary = g_variant_ref_sink (g_variant_new_from_bytes (OSTREE_SUMMARY_GVARIANT_FORMAT, summary_bytes, FALSE));
+ GHashTableIter iter;
+ const OstreeCollectionRef *ref;
+ const gchar *checksum;
+
+ 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;
+
+ /* Check that at least one of the refs has a non-%NULL checksum set, otherwise
+ * we can discard this peer. */
+ g_hash_table_iter_init (&iter, supported_ref_to_checksum);
+ while (g_hash_table_iter_next (&iter,
+ (gpointer *) &ref,
+ (gpointer *) &checksum))
+ {
+ if (checksum != NULL)
+ return TRUE;
+ }
+
+ g_set_error (error, G_IO_ERROR, G_IO_ERROR_NOT_FOUND,
+ "No matching refs were found in the summary file");
+ return FALSE;
+}
+
+/* Download the summary file from @remote, and return the bytes of the file in
+ * @out_summary_bytes. */
+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;
+
+ return get_refs_and_checksums_from_summary (summary_bytes, supported_ref_to_checksum, 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) */
+ SoupURI *_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 = soup_uri_new (NULL);
+ soup_uri_set_scheme (_uri, "http");
+ soup_uri_set_host (_uri, service->address);
+ soup_uri_set_port (_uri, service->port);
+ soup_uri_set_path (_uri, repo_path);
+ uri = soup_uri_to_string (_uri, FALSE);
+ soup_uri_free (_uri);
+
+ for (i = 0; i < possible_refs->len; i++)
+ {
+ const OstreeCollectionRef *ref = g_ptr_array_index (possible_refs, i);
+ g_autofree gchar *keyring = NULL;
+ g_autoptr(UriAndKeyring) resolved_repo = NULL;
+
+ /* Look up the GPG keyring for this ref. */
+ keyring = ostree_repo_resolve_keyring_for_collection (parent_repo, ref->collection_id,
+ cancellable, &error);
+
+ if (keyring == 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’.",
+ ref->collection_id, ref->ref_name, uri, keyring);
+
+ resolved_repo = uri_and_keyring_new (uri, keyring);
+
+ 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 (name);
+
+ g_clear_pointer (&remote->keyring, g_free);
+ remote->keyring = g_strdup (repo->keyring);
+
+ 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", TRUE);
+
+ 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,
+ (summary_timestamp != NULL) ? GUINT64_FROM_BE (g_variant_get_uint64 (summary_timestamp)) : 0));
+ }
+}
+
+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: 2017.8
+ */
+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: 2017.8
+ */
+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),
+ AVAHI_CLIENT_NO_FAIL,
+ client_cb, self, &failure);
+
+ if (client == NULL)
+ {
+ 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: 2017.8
+ */
+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 */
diff --git a/src/libostree/ostree-repo-finder-avahi.h b/src/libostree/ostree-repo-finder-avahi.h
new file mode 100644
index 00000000..98d37723
--- /dev/null
+++ b/src/libostree/ostree-repo-finder-avahi.h
@@ -0,0 +1,62 @@
+/* -*- mode: C; c-file-style: "gnu"; indent-tabs-mode: nil; -*-
+ *
+ * Copyright © 2017 Endless Mobile, Inc.
+ *
+ * This library is free software; you can redistribute it and/or
+ * modify it under the terms of the GNU Lesser General Public
+ * License as published by the Free Software Foundation; either
+ * version 2 of the License, or (at your option) any later version.
+ *
+ * This library is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
+ * Lesser General Public License for more details.
+ *
+ * You should have received a copy of the GNU Lesser General Public
+ * License along with this library; if not, write to the
+ * Free Software Foundation, Inc., 59 Temple Place - Suite 330,
+ * Boston, MA 02111-1307, USA.
+ *
+ * Authors:
+ * - Philip Withnall
+ */
+
+#pragma once
+
+#include
+#include
+#include
+
+#include "ostree-repo-finder.h"
+#include "ostree-types.h"
+
+G_BEGIN_DECLS
+
+#define OSTREE_TYPE_REPO_FINDER_AVAHI (ostree_repo_finder_avahi_get_type ())
+
+/* Manually expanded version of the following, omitting autoptr support (for GLib < 2.44):
+_OSTREE_PUBLIC
+G_DECLARE_FINAL_TYPE (OstreeRepoFinderAvahi, ostree_repo_finder_avahi, OSTREE, REPO_FINDER_AVAHI, GObject) */
+
+_OSTREE_PUBLIC
+GType ostree_repo_finder_avahi_get_type (void);
+
+G_GNUC_BEGIN_IGNORE_DEPRECATIONS
+typedef struct _OstreeRepoFinderAvahi OstreeRepoFinderAvahi;
+typedef struct { GObjectClass parent_class; } OstreeRepoFinderAvahiClass;
+
+static inline OstreeRepoFinderAvahi *OSTREE_REPO_FINDER_AVAHI (gpointer ptr) { return G_TYPE_CHECK_INSTANCE_CAST (ptr, ostree_repo_finder_avahi_get_type (), OstreeRepoFinderAvahi); }
+static inline gboolean OSTREE_IS_REPO_FINDER_AVAHI (gpointer ptr) { return G_TYPE_CHECK_INSTANCE_TYPE (ptr, ostree_repo_finder_avahi_get_type ()); }
+G_GNUC_END_IGNORE_DEPRECATIONS
+
+_OSTREE_PUBLIC
+OstreeRepoFinderAvahi *ostree_repo_finder_avahi_new (GMainContext *context);
+
+_OSTREE_PUBLIC
+void ostree_repo_finder_avahi_start (OstreeRepoFinderAvahi *self,
+ GError **error);
+
+_OSTREE_PUBLIC
+void ostree_repo_finder_avahi_stop (OstreeRepoFinderAvahi *self);
+
+G_END_DECLS
diff --git a/src/libostree/ostree-repo-pull.c b/src/libostree/ostree-repo-pull.c
index ecc9b72c..4c87199a 100644
--- a/src/libostree/ostree-repo-pull.c
+++ b/src/libostree/ostree-repo-pull.c
@@ -43,6 +43,9 @@
#include "ostree-repo-finder.h"
#include "ostree-repo-finder-config.h"
#include "ostree-repo-finder-mount.h"
+#ifdef HAVE_AVAHI
+#include "ostree-repo-finder-avahi.h"
+#endif /* HAVE_AVAHI */
#endif /* OSTREE_ENABLE_EXPERIMENTAL_API */
#include
@@ -3961,11 +3964,13 @@ typedef struct
OstreeCollectionRef **refs;
GVariant *options;
OstreeAsyncProgress *progress;
+ OstreeRepoFinder *default_finder_avahi;
} FindRemotesData;
static void
find_remotes_data_free (FindRemotesData *data)
{
+ g_clear_object (&data->default_finder_avahi);
g_clear_object (&data->progress);
g_clear_pointer (&data->options, g_variant_unref);
ostree_collection_ref_freev (data->refs);
@@ -3978,7 +3983,8 @@ G_DEFINE_AUTOPTR_CLEANUP_FUNC (FindRemotesData, find_remotes_data_free)
static FindRemotesData *
find_remotes_data_new (const OstreeCollectionRef * const *refs,
GVariant *options,
- OstreeAsyncProgress *progress)
+ OstreeAsyncProgress *progress,
+ OstreeRepoFinder *default_finder_avahi)
{
g_autoptr(FindRemotesData) data = NULL;
@@ -3986,6 +3992,7 @@ find_remotes_data_new (const OstreeCollectionRef * const *refs,
data->refs = ostree_collection_ref_dupv (refs);
data->options = (options != NULL) ? g_variant_ref (options) : NULL;
data->progress = (progress != NULL) ? g_object_ref (progress) : NULL;
+ data->default_finder_avahi = (default_finder_avahi != NULL) ? g_object_ref (default_finder_avahi) : NULL;
return g_steal_pointer (&data);
}
@@ -4085,10 +4092,10 @@ ostree_repo_find_remotes_async (OstreeRepo *self,
{
g_autoptr(GTask) task = NULL;
g_autoptr(FindRemotesData) data = NULL;
- GMainContext *context;
OstreeRepoFinder *default_finders[4] = { NULL, };
g_autoptr(OstreeRepoFinder) finder_config = NULL;
g_autoptr(OstreeRepoFinder) finder_mount = NULL;
+ g_autoptr(OstreeRepoFinder) finder_avahi = NULL;
g_return_if_fail (OSTREE_IS_REPO (self));
g_return_if_fail (is_valid_collection_ref_array (refs));
@@ -4102,21 +4109,43 @@ ostree_repo_find_remotes_async (OstreeRepo *self,
task = g_task_new (self, cancellable, callback, user_data);
g_task_set_source_tag (task, ostree_repo_find_remotes_async);
- context = g_main_context_get_thread_default ();
-
/* Are we using #OstreeRepoFinders provided by the user, or the defaults? */
if (finders == NULL)
{
+#ifdef HAVE_AVAHI
+ GMainContext *context = g_main_context_get_thread_default ();
+ g_autoptr(GError) local_error = NULL;
+#endif /* HAVE_AVAHI */
+
finder_config = OSTREE_REPO_FINDER (ostree_repo_finder_config_new ());
finder_mount = OSTREE_REPO_FINDER (ostree_repo_finder_mount_new (NULL));
+#ifdef HAVE_AVAHI
+ finder_avahi = OSTREE_REPO_FINDER (ostree_repo_finder_avahi_new (context));
+#endif /* HAVE_AVAHI */
default_finders[0] = finder_config;
default_finders[1] = finder_mount;
+ default_finders[2] = finder_avahi;
finders = default_finders;
+
+#ifdef HAVE_AVAHI
+ ostree_repo_finder_avahi_start (OSTREE_REPO_FINDER_AVAHI (finder_avahi),
+ &local_error);
+
+ if (local_error != NULL)
+ {
+ g_warning ("Avahi finder failed; removing it: %s", local_error->message);
+ default_finders[2] = NULL;
+ g_clear_object (&finder_avahi);
+ }
+#endif /* HAVE_AVAHI */
}
- data = find_remotes_data_new (refs, options, progress);
+ /* We need to keep a pointer to the default Avahi finder so we can stop it
+ * again after the operation, which happens implicitly by dropping the final
+ * ref. */
+ data = find_remotes_data_new (refs, options, progress, finder_avahi);
g_task_set_task_data (task, g_steal_pointer (&data), (GDestroyNotify) find_remotes_data_free);
/* Asynchronously resolve all possible remotes for the given refs. */
diff --git a/src/libostree/ostree.h b/src/libostree/ostree.h
index 0fe2a23e..0f727384 100644
--- a/src/libostree/ostree.h
+++ b/src/libostree/ostree.h
@@ -38,6 +38,7 @@
#ifdef OSTREE_ENABLE_EXPERIMENTAL_API
#include
#include
+#include
#include
#include
#endif /* OSTREE_ENABLE_EXPERIMENTAL_API */
diff --git a/tests/.gitignore b/tests/.gitignore
index 5ece7ea1..9bec67a3 100644
--- a/tests/.gitignore
+++ b/tests/.gitignore
@@ -15,6 +15,7 @@ test-mutable-tree
test-ot-opt-utils
test-ot-tool-util
test-ot-unix-utils
+test-repo-finder-avahi
test-repo-finder-config
test-repo-finder-mount
test-rollsum-cli
diff --git a/tests/test-repo-finder-avahi.c b/tests/test-repo-finder-avahi.c
new file mode 100644
index 00000000..b2fddf70
--- /dev/null
+++ b/tests/test-repo-finder-avahi.c
@@ -0,0 +1,228 @@
+/* -*- mode: C; c-file-style: "gnu"; indent-tabs-mode: nil; -*-
+ *
+ * Copyright © 2017 Endless Mobile, Inc.
+ *
+ * This library is free software; you can redistribute it and/or
+ * modify it under the terms of the GNU Lesser General Public
+ * License as published by the Free Software Foundation; either
+ * version 2 of the License, or (at your option) any later version.
+ *
+ * This library is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
+ * Lesser General Public License for more details.
+ *
+ * You should have received a copy of the GNU Lesser General Public
+ * License along with this library; if not, write to the
+ * Free Software Foundation, Inc., 59 Temple Place - Suite 330,
+ * Boston, MA 02111-1307, USA.
+ *
+ * Authors:
+ * - Philip Withnall
+ */
+
+#include "config.h"
+
+#include
+#include
+#include
+#include
+#include
+
+#include "ostree-autocleanups.h"
+#include "ostree-repo-finder.h"
+#include "ostree-repo-finder-avahi.h"
+#include "ostree-repo-finder-avahi-private.h"
+
+/* FIXME: Upstream this */
+G_DEFINE_AUTOPTR_CLEANUP_FUNC (AvahiStringList, avahi_string_list_free)
+
+/* Test the object constructor works at a basic level. */
+static void
+test_repo_finder_avahi_init (void)
+{
+ g_autoptr(OstreeRepoFinderAvahi) finder = NULL;
+ g_autoptr(GMainContext) context = NULL;
+
+ /* Default main context. */
+ finder = ostree_repo_finder_avahi_new (NULL);
+ g_clear_object (&finder);
+
+ /* Explicit main context. */
+ context = g_main_context_new ();
+ finder = ostree_repo_finder_avahi_new (context);
+ g_clear_object (&finder);
+}
+
+/* Test parsing valid and invalid TXT records. */
+static void
+test_repo_finder_avahi_txt_records_parse (void)
+{
+ struct
+ {
+ const guint8 *txt;
+ gsize txt_len;
+ const gchar *expected_key; /* (nullable) to indicate parse failure */
+ const guint8 *expected_value; /* (nullable) to allow for valueless keys */
+ gsize expected_value_len;
+ }
+ vectors[] =
+ {
+ { (const guint8 *) "", 0, NULL, NULL, 0 },
+ { (const guint8 *) "\x00", 1, NULL, NULL, 0 },
+ { (const guint8 *) "\xff", 1, NULL, NULL, 0 },
+ { (const guint8 *) "k\x00", 2, NULL, NULL, 0 },
+ { (const guint8 *) "k\xff", 2, NULL, NULL, 0 },
+ { (const guint8 *) "=", 1, NULL, NULL, 0 },
+ { (const guint8 *) "=value", 6, NULL, NULL, 0 },
+ { (const guint8 *) "k=v", 3, "k", (const guint8 *) "v", 1 },
+ { (const guint8 *) "key=value", 9, "key", (const guint8 *) "value", 5 },
+ { (const guint8 *) "k=v=", 4, "k", (const guint8 *) "v=", 2 },
+ { (const guint8 *) "k=", 2, "k", (const guint8 *) "", 0 },
+ { (const guint8 *) "k", 1, "k", NULL, 0 },
+ { (const guint8 *) "k==", 3, "k", (const guint8 *) "=", 1 },
+ { (const guint8 *) "k=\x00\x01\x02", 5, "k", (const guint8 *) "\x00\x01\x02", 3 },
+ };
+ gsize i;
+
+ for (i = 0; i < G_N_ELEMENTS (vectors); i++)
+ {
+ g_autoptr(AvahiStringList) string_list = NULL;
+ g_autoptr(GHashTable) attributes = NULL;
+
+ g_test_message ("Vector %" G_GSIZE_FORMAT, i);
+
+ string_list = avahi_string_list_add_arbitrary (NULL, vectors[i].txt, vectors[i].txt_len);
+
+ attributes = _ostree_txt_records_parse (string_list);
+
+ if (vectors[i].expected_key != NULL)
+ {
+ GBytes *value;
+ g_autoptr(GBytes) expected_value = NULL;
+
+ g_assert_true (g_hash_table_lookup_extended (attributes,
+ vectors[i].expected_key,
+ NULL,
+ (gpointer *) &value));
+ g_assert_cmpuint (g_hash_table_size (attributes), ==, 1);
+
+ if (vectors[i].expected_value != NULL)
+ {
+ g_assert_nonnull (value);
+ expected_value = g_bytes_new_static (vectors[i].expected_value, vectors[i].expected_value_len);
+ g_assert_true (g_bytes_equal (value, expected_value));
+ }
+ else
+ {
+ g_assert_null (value);
+ }
+ }
+ else
+ {
+ g_assert_cmpuint (g_hash_table_size (attributes), ==, 0);
+ }
+ }
+}
+
+/* Test that the first value for a set of duplicate records is returned.
+ * See RFC 6763, §6.4. */
+static void
+test_repo_finder_avahi_txt_records_duplicates (void)
+{
+ g_autoptr(AvahiStringList) string_list = NULL;
+ g_autoptr(GHashTable) attributes = NULL;
+ GBytes *value;
+ g_autoptr(GBytes) expected_value = NULL;
+
+ /* Reverse the list before using it, as they are built in reverse order.
+ * (See the #AvahiStringList documentation.) */
+ string_list = avahi_string_list_new ("k=value1", "k=value2", "k=value3", NULL);
+ string_list = avahi_string_list_reverse (string_list);
+ attributes = _ostree_txt_records_parse (string_list);
+
+ g_assert_cmpuint (g_hash_table_size (attributes), ==, 1);
+ value = g_hash_table_lookup (attributes, "k");
+ g_assert_nonnull (value);
+
+ expected_value = g_bytes_new_static ("value1", strlen ("value1"));
+ g_assert_true (g_bytes_equal (value, expected_value));
+}
+
+/* Test that keys are parsed and looked up case insensitively.
+ * See RFC 6763, §6.4. */
+static void
+test_repo_finder_avahi_txt_records_case_sensitivity (void)
+{
+ g_autoptr(AvahiStringList) string_list = NULL;
+ g_autoptr(GHashTable) attributes = NULL;
+ GBytes *value1, *value2;
+ g_autoptr(GBytes) expected_value1 = NULL, expected_value2 = NULL;
+
+ /* Reverse the list before using it, as they are built in reverse order.
+ * (See the #AvahiStringList documentation.) */
+ string_list = avahi_string_list_new ("k=value1",
+ "K=value2",
+ "KeY2=v",
+ NULL);
+ string_list = avahi_string_list_reverse (string_list);
+ attributes = _ostree_txt_records_parse (string_list);
+
+ g_assert_cmpuint (g_hash_table_size (attributes), ==, 2);
+
+ value1 = g_hash_table_lookup (attributes, "k");
+ g_assert_nonnull (value1);
+ expected_value1 = g_bytes_new_static ("value1", strlen ("value1"));
+ g_assert_true (g_bytes_equal (value1, expected_value1));
+
+ g_assert_null (g_hash_table_lookup (attributes, "K"));
+
+ value2 = g_hash_table_lookup (attributes, "key2");
+ g_assert_nonnull (value2);
+ expected_value2 = g_bytes_new_static ("v", 1);
+ g_assert_true (g_bytes_equal (value2, expected_value2));
+
+ g_assert_null (g_hash_table_lookup (attributes, "KeY2"));
+}
+
+/* Test that keys which have an empty value can be distinguished from those
+ * which have no value. See RFC 6763, §6.4. */
+static void
+test_repo_finder_avahi_txt_records_empty_and_missing (void)
+{
+ g_autoptr(AvahiStringList) string_list = NULL;
+ g_autoptr(GHashTable) attributes = NULL;
+ GBytes *value1, *value2;
+ g_autoptr(GBytes) expected_value1 = NULL;
+
+ string_list = avahi_string_list_new ("empty=",
+ "missing",
+ NULL);
+ attributes = _ostree_txt_records_parse (string_list);
+
+ g_assert_cmpuint (g_hash_table_size (attributes), ==, 2);
+
+ value1 = g_hash_table_lookup (attributes, "empty");
+ g_assert_nonnull (value1);
+ expected_value1 = g_bytes_new_static ("", 0);
+ g_assert_true (g_bytes_equal (value1, expected_value1));
+
+ g_assert_true (g_hash_table_lookup_extended (attributes, "missing", NULL, (gpointer *) &value2));
+ g_assert_null (value2);
+}
+
+int main (int argc, char **argv)
+{
+ setlocale (LC_ALL, "");
+ g_test_init (&argc, &argv, NULL);
+
+ g_test_add_func ("/repo-finder-avahi/init", test_repo_finder_avahi_init);
+ g_test_add_func ("/repo-finder-avahi/txt-records/parse", test_repo_finder_avahi_txt_records_parse);
+ g_test_add_func ("/repo-finder-avahi/txt-records/duplicates", test_repo_finder_avahi_txt_records_duplicates);
+ g_test_add_func ("/repo-finder-avahi/txt-records/case-sensitivity", test_repo_finder_avahi_txt_records_case_sensitivity);
+ g_test_add_func ("/repo-finder-avahi/txt-records/empty-and-missing", test_repo_finder_avahi_txt_records_empty_and_missing);
+ /* FIXME: Add tests for service processing, probably by splitting the
+ * code in OstreeRepoFinderAvahi around found_services. */
+
+ return g_test_run();
+}