lib/repo-finder-mount: Change the schema for finding repos on volumes
authorPhilip Withnall <withnall@endlessm.com>
Fri, 15 Sep 2017 14:59:32 +0000 (15:59 +0100)
committerAtomic Bot <atomic-devel@projectatomic.io>
Tue, 19 Sep 2017 14:51:09 +0000 (14:51 +0000)
See issue #1174 for the rationale behind this. In summary:
 • It required two lists of collection–refs to be maintained: one in the
   repository, and one pointing to the repository.
 • It didn’t automatically work for live USBs of OSs based on OSTree
   (where there’s always a repository at /ostree/repo).
 • It was unnecessarily complex.

The new scheme allows a list of repositories to be searched, but without
needing a layer of indirection through their collection–refs. It adds
/ostree/repo and /.ostree/repo as well-known repository locations which
are always checked on a mounted volume (if they exist).

Update the unit tests accordingly.

Signed-off-by: Philip Withnall <withnall@endlessm.com>
https://github.com/ostreedev/ostree/issues/1174

Closes: #1179
Approved by: cgwalters

src/libostree/ostree-repo-finder-mount.c
tests/test-repo-finder-mount.c

index b7b15bf08f90c9d753050d166ed44c2d9db3dcfa..31bb8bfcd37b528e092a24e16e558c6446f0b50f 100644 (file)
  * #OstreeRepoFinderMount is an implementation of #OstreeRepoFinder which looks
  * refs up in well-known locations on any mounted removable volumes.
  *
- * For an #OstreeCollectionRef, (`C`, `R`), it checks whether `.ostree/repos/C/R`
- * exists and is an OSTree repository on each mounted removable volume. Collection
- * IDs and ref names are not escaped when building the path, so if either
- * contains `/` in its name, the repository will be checked for in a
- * subdirectory of `.ostree/repos`. Non-removable volumes are ignored.
+ * For each mounted removable volume, the directory `.ostree/repos.d` will be
+ * enumerated, and all OSTree repositories below it will be searched, in lexical
+ * order, for the requested #OstreeCollectionRefs. The names of the directories
+ * below `.ostree/repos.d` are irrelevant, apart from their lexical ordering.
+ * The directory `ostree/repo` will be searched after the others, if it exists.
+ * Non-removable volumes are ignored.
  *
  * 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/heads` and
  * `refs/mirrors` in the repository on the removable volume.
  *
- * Symlinks are followed when resolving the refs, so a volume might contain a
- * single OSTree at some arbitrary path, with a number of refs linking to it
- * from `.ostree/repos`. Any symlink which points outside the volume’s file
+ * Symlinks are followed when listing the repositories, so a volume might
+ * contain a single OSTree at some arbitrary path, with a symlink from
+ * `.ostree/repos.d`. Any symlink which points outside the volume’s file
  * system will be ignored. Repositories are deduplicated in the results.
  *
  * The volume monitor used to find mounted volumes can be overridden by setting
@@ -166,6 +167,137 @@ results_compare_cb (gconstpointer a,
   return ostree_repo_finder_result_compare (result_a, result_b);
 }
 
+typedef struct
+{
+  char *ordering_name;  /* (owned) */
+  OstreeRepo *repo;  /* (owned) */
+  GHashTable *refs;  /* (owned) (element-type OstreeCollectionRef utf8) */
+} RepoAndRefs;
+
+static void
+repo_and_refs_clear (RepoAndRefs *data)
+{
+  g_hash_table_unref (data->refs);
+  g_object_unref (data->repo);
+  g_free (data->ordering_name);
+}
+
+static gint
+repo_and_refs_compare (gconstpointer a,
+                       gconstpointer b)
+{
+  const RepoAndRefs *_a = a;
+  const RepoAndRefs *_b = b;
+
+  return strcmp (_a->ordering_name, _b->ordering_name);
+}
+
+/* Check whether the repo at @dfd/@path is within the given mount, is not equal
+ * to the @parent_repo, and can be opened. If so, return it as @out_repo and
+ * all its collection–refs as @out_refs, to be added into the results. */
+static gboolean
+scan_repo (int                 dfd,
+           const char         *path,
+           const char         *mount_name,
+           const struct stat  *mount_root_stbuf,
+           OstreeRepo         *parent_repo,
+           OstreeRepo        **out_repo,
+           GHashTable        **out_refs,
+           GCancellable       *cancellable,
+           GError            **error)
+{
+  g_autoptr(GError) local_error = NULL;
+
+  g_autoptr(OstreeRepo) repo = ostree_repo_open_at (dfd, path, cancellable, &local_error);
+  if (repo == NULL)
+    {
+      g_debug ("Ignoring repository ‘%s’ on mount ‘%s’ as it could not be opened: %s",
+               path, mount_name, local_error->message);
+      g_propagate_error (error, g_steal_pointer (&local_error));
+      return FALSE;
+    }
+
+  int repo_dfd = ostree_repo_get_dfd (repo);
+  struct stat stbuf;
+
+  if (!glnx_fstat (repo_dfd, &stbuf, &local_error))
+    {
+      g_debug ("Ignoring repository ‘%s’ on mount ‘%s’ as querying its info failed: %s",
+               path, mount_name, local_error->message);
+      g_propagate_error (error, g_steal_pointer (&local_error));
+      return FALSE;
+    }
+
+  /* Check the resolved repository path is below the mount point. Do not
+   * allow ref symlinks to point somewhere outside of the mounted volume. */
+  if (stbuf.st_dev != mount_root_stbuf->st_dev)
+    {
+      g_debug ("Ignoring repository ‘%s’ on mount ‘%s’ as it’s on a different file system from the mount",
+               path, mount_name);
+      g_propagate_error (error, g_steal_pointer (&local_error));
+      return FALSE;
+    }
+
+  /* Exclude repositories which resolve to @parent_repo. */
+  if (stbuf.st_dev == parent_repo->device &&
+      stbuf.st_ino == parent_repo->inode)
+    {
+      g_debug ("Ignoring repository ‘%s’ on mount ‘%s’ as it is the same as the one we are resolving",
+               path, mount_name);
+      g_propagate_error (error, g_steal_pointer (&local_error));
+      return FALSE;
+    }
+
+  /* List the repo’s refs and return them. */
+  g_autoptr(GHashTable) repo_refs = NULL;  /* (element-type OstreeCollectionRef utf8) */
+
+  if (!ostree_repo_list_collection_refs (repo, NULL, &repo_refs,
+                                         OSTREE_REPO_LIST_REFS_EXT_EXCLUDE_REMOTES,
+                                         cancellable, &local_error))
+    {
+      g_debug ("Ignoring repository ‘%s’ on mount ‘%s’ as its refs could not be listed: %s",
+               path, mount_name, local_error->message);
+      g_propagate_error (error, g_steal_pointer (&local_error));
+      return FALSE;
+    }
+
+  if (out_repo != NULL)
+    *out_repo = g_steal_pointer (&repo);
+  if (out_refs != NULL)
+    *out_refs = g_steal_pointer (&repo_refs);
+
+  return TRUE;
+}
+
+static void
+scan_and_add_repo (int                 dfd,
+                   const char         *path,
+                   gboolean            sortable,
+                   const char         *mount_name,
+                   const struct stat  *mount_root_stbuf,
+                   OstreeRepo         *parent_repo,
+                   GArray             *inout_repos_refs,
+                   GCancellable       *cancellable)
+{
+  g_autoptr(GHashTable) repo_refs = NULL;
+  g_autoptr(OstreeRepo) repo = NULL;
+
+  if (scan_repo (dfd, path,
+                 mount_name, mount_root_stbuf,
+                 parent_repo, &repo, &repo_refs, cancellable, NULL))
+    {
+      RepoAndRefs val = {
+        sortable ? g_strdup (path) : NULL,
+        g_steal_pointer (&repo),
+        g_steal_pointer (&repo_refs)
+      };
+      g_array_append_val (inout_repos_refs, val);
+
+      g_debug ("%s: Adding repo ‘%s’ (%ssortable)",
+               G_STRFUNC, path, sortable ? "" : "not ");
+    }
+}
+
 static void
 ostree_repo_finder_mount_resolve_async (OstreeRepoFinder                  *finder,
                                         const OstreeCollectionRef * const *refs,
@@ -214,7 +346,6 @@ ostree_repo_finder_mount_resolve_async (OstreeRepoFinder                  *finde
           continue;
         }
 
-      /* Check if it contains a .ostree/repos directory. */
       mount_root = g_mount_get_root (mount);
       mount_root_path = g_file_get_path (mount_root);
 
@@ -225,18 +356,6 @@ ostree_repo_finder_mount_resolve_async (OstreeRepoFinder                  *finde
           continue;
         }
 
-      if (!glnx_opendirat (mount_root_dfd, ".ostree/repos", TRUE, &repos_dfd, &local_error))
-        {
-          if (g_error_matches (local_error, G_IO_ERROR, G_IO_ERROR_NOT_FOUND))
-            g_debug ("Ignoring mount ‘%s’ as ‘%s/.ostree/repos’ directory doesn’t exist.",
-                     mount_name, mount_root_path);
-          else
-            g_debug ("Ignoring mount ‘%s’ as ‘%s/.ostree/repos’ directory can’t be opened: %s",
-                     mount_name, mount_root_path, local_error->message);
-
-          continue;
-        }
-
       /* stat() the mount root so we can later check whether the resolved
        * repositories for individual refs are on the same device (to avoid the
        * symlinks for them pointing outside the mount root). */
@@ -247,6 +366,64 @@ ostree_repo_finder_mount_resolve_async (OstreeRepoFinder                  *finde
           continue;
         }
 
+      /* Check if it contains a .ostree/repos.d directory. If not, move on and
+       * try the other well-known subdirectories. */
+      if (!glnx_opendirat (mount_root_dfd, ".ostree/repos.d", TRUE, &repos_dfd, NULL))
+        repos_dfd = -1;
+
+      /* List all the repositories in the repos.d directory. */
+      /* (element-type GHashTable (element-type OstreeCollectionRef utf8)) */
+      g_autoptr(GArray) repos_refs = g_array_new (FALSE, TRUE, sizeof (RepoAndRefs));
+      g_array_set_clear_func (repos_refs, (GDestroyNotify) repo_and_refs_clear);
+
+      GLnxDirFdIterator repos_iter;
+
+      if (repos_dfd >= 0 &&
+          !glnx_dirfd_iterator_init_at (repos_dfd, ".", TRUE, &repos_iter, &local_error))
+        {
+          g_debug ("Error iterating over ‘%s/.ostree/repos.d’ directory in mount ‘%s’: %s",
+                   mount_root_path, mount_name, local_error->message);
+          g_clear_error (&local_error);
+          /* don’t skip this mount as there’s still the ostree/repo directory to try */
+        }
+      else if (repos_dfd >= 0)
+        {
+          while (TRUE)
+            {
+              struct dirent *repo_dent;
+
+              if (!glnx_dirfd_iterator_next_dent (&repos_iter, &repo_dent, cancellable, &local_error))
+                {
+                  g_debug ("Error iterating over ‘%s/.ostree/repos.d’ directory in mount ‘%s’: %s",
+                           mount_root_path, mount_name, local_error->message);
+                  g_clear_error (&local_error);
+                  /* don’t skip this mount as there’s still the ostree/repo directory to try */
+                  break;
+                }
+
+              if (repo_dent == NULL)
+                break;
+
+              /* Grab the set of collection–refs from the repo if we can open it. */
+              scan_and_add_repo (repos_dfd, repo_dent->d_name, TRUE,
+                                 mount_name, &mount_root_stbuf,
+                                 parent_repo, repos_refs, cancellable);
+            }
+        }
+
+      /* Sort the repos lexically. */
+      g_array_sort (repos_refs, repo_and_refs_compare);
+
+      /* Also check the .ostree/repo and ostree/repo directories in the mount,
+       * as well-known special cases. Add them after sorting, so they’re always
+       * last. */
+      scan_and_add_repo (mount_root_dfd, ".ostree/repo", FALSE,
+                         mount_name, &mount_root_stbuf,
+                         parent_repo, repos_refs, cancellable);
+      scan_and_add_repo (mount_root_dfd, "ostree/repo", FALSE,
+                         mount_name, &mount_root_stbuf,
+                         parent_repo, repos_refs, cancellable);
+
       /* Check whether a subdirectory exists for any of the @refs we’re looking
        * for. If so, and it’s a symbolic link, dereference it so multiple links
        * to the same repository (containing multiple refs) are coalesced.
@@ -256,122 +433,67 @@ ostree_repo_finder_mount_resolve_async (OstreeRepoFinder                  *finde
 
       for (i = 0; refs[i] != NULL; i++)
         {
-          struct stat stbuf;
-          g_autofree gchar *collection_and_ref = NULL;
+          const OstreeCollectionRef *ref = refs[i];
           g_autofree gchar *resolved_repo_uri = NULL;
           g_autofree gchar *keyring = NULL;
           g_autoptr(UriAndKeyring) resolved_repo = NULL;
 
-          collection_and_ref = g_build_filename (refs[i]->collection_id, refs[i]->ref_name, NULL);
-
-          if (!glnx_fstatat (repos_dfd, collection_and_ref, &stbuf, AT_NO_AUTOMOUNT, &local_error))
-            {
-              g_debug ("Ignoring ref (%s, %s) on mount ‘%s’ as querying info of ‘%s’ failed: %s",
-                       refs[i]->collection_id, refs[i]->ref_name, mount_name, collection_and_ref, local_error->message);
-              g_clear_error (&local_error);
-              continue;
-            }
-
-          if ((stbuf.st_mode & S_IFMT) != S_IFDIR)
-            {
-              g_debug ("Ignoring ref (%s, %s) on mount ‘%s’ as ‘%s’ is of type %u, not a directory.",
-                       refs[i]->collection_id, refs[i]->ref_name, mount_name, collection_and_ref, (stbuf.st_mode & S_IFMT));
-              g_clear_error (&local_error);
-              continue;
-            }
-
-          /* Check the resolved repository path is below the mount point. Do not
-           * allow ref symlinks to point somewhere outside of the mounted
-           * volume. */
-          if (stbuf.st_dev != mount_root_stbuf.st_dev)
-            {
-              g_debug ("Ignoring ref (%s, %s) on mount ‘%s’ as it’s on a different file system from the mount.",
-                       refs[i]->collection_id, refs[i]->ref_name, mount_name);
-              g_clear_error (&local_error);
-              continue;
-            }
-
-          /* Exclude repositories which resolve to @parent_repo. */
-          if (stbuf.st_dev == parent_repo->device &&
-              stbuf.st_ino == parent_repo->inode)
+          for (gsize j = 0; j < repos_refs->len; j++)
             {
-              g_debug ("Ignoring ref (%s, %s) on mount ‘%s’ as it is the same as the one we are resolving",
-                       refs[i]->collection_id, refs[i]->ref_name, mount_name);
-              g_clear_error (&local_error);
-              continue;
+              const RepoAndRefs *repo_and_refs = &g_array_index (repos_refs, RepoAndRefs, j);
+              OstreeRepo *repo = repo_and_refs->repo;
+              GHashTable *repo_refs = repo_and_refs->refs;
+              g_autofree char *repo_path = g_file_get_path (ostree_repo_get_path (repo));
+
+              const gchar *checksum = g_hash_table_lookup (repo_refs, ref);
+
+              if (checksum == NULL)
+                {
+                  g_debug ("Ignoring repository ‘%s’ when looking for ref (%s, %s) on mount ‘%s’ as it doesn’t contain the ref.",
+                           repo_path, ref->collection_id, ref->ref_name, mount_name);
+                  g_clear_error (&local_error);
+                  continue;
+                }
+
+              /* Finally, look up the GPG keyring for this ref. */
+              keyring = ostree_repo_resolve_keyring_for_collection (parent_repo, ref->collection_id,
+                                                                    cancellable, &local_error);
+
+              if (keyring == NULL)
+                {
+                  g_debug ("Ignoring repository ‘%s’ when looking for ref (%s, %s) on mount ‘%s’ due to missing keyring: %s",
+                           repo_path, ref->collection_id, ref->ref_name, mount_name, local_error->message);
+                  g_clear_error (&local_error);
+                  continue;
+                }
+
+              /* There is a valid repo at (or pointed to by)
+               * $mount_root/.ostree/repos.d/$something.
+               * Add it to the results, keyed by the canonicalised repository URI
+               * to deduplicate the results. */
+              g_autofree char *canonical_repo_path = realpath (repo_path, NULL);
+              resolved_repo_uri = g_strconcat ("file://", canonical_repo_path, NULL);
+              g_debug ("Resolved ref (%s, %s) on mount ‘%s’ to repo URI ‘%s’ with keyring ‘%s’.",
+                       ref->collection_id, ref->ref_name, mount_name, resolved_repo_uri, keyring);
+
+              resolved_repo = uri_and_keyring_new (resolved_repo_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 */);
+                }
+
+              g_hash_table_insert (supported_ref_to_checksum, (gpointer) ref, g_strdup (checksum));
+
+              /* We’ve found a result for this collection–ref. No point in checking
+               * the other repos on the mount, since pulling in parallel from them won’t help. */
+              break;
             }
-
-          /* Grab the given ref and a checksum for it from the repo, if it appears to be a valid repo */
-          g_autoptr(OstreeRepo) repo = ostree_repo_open_at (repos_dfd, collection_and_ref,
-                                                            cancellable, &local_error);
-          if (!repo)
-            {
-              g_debug ("Ignoring ref (%s, %s) on mount ‘%s’ as its repository could not be opened: %s",
-                       refs[i]->collection_id, refs[i]->ref_name, mount_name, local_error->message);
-              g_clear_error (&local_error);
-              continue;
-            }
-
-          g_autoptr(GHashTable) repo_refs = NULL;  /* (element-type OstreeCollectionRef utf8) */
-
-          if (!ostree_repo_list_collection_refs (repo, refs[i]->collection_id, &repo_refs,
-                                                 OSTREE_REPO_LIST_REFS_EXT_EXCLUDE_REMOTES,
-                                                 cancellable, &local_error))
-            {
-              g_debug ("Ignoring ref (%s, %s) on mount ‘%s’ as its refs could not be listed: %s",
-                       refs[i]->collection_id, refs[i]->ref_name, mount_name, local_error->message);
-              g_clear_error (&local_error);
-              continue;
-            }
-
-          const gchar *checksum = g_hash_table_lookup (repo_refs, refs[i]);
-
-          if (checksum == NULL)
-            {
-              g_debug ("Ignoring ref (%s, %s) on mount ‘%s’ as its repository doesn’t contain the ref.",
-                       refs[i]->collection_id, refs[i]->ref_name, mount_name);
-              g_clear_error (&local_error);
-              continue;
-            }
-
-          /* Finally, look up the GPG keyring for this ref. */
-          keyring = ostree_repo_resolve_keyring_for_collection (parent_repo, refs[i]->collection_id,
-                                                                cancellable, &local_error);
-
-          if (keyring == NULL)
-            {
-              g_debug ("Ignoring ref (%s, %s) on mount ‘%s’ due to missing keyring: %s",
-                       refs[i]->collection_id, refs[i]->ref_name, mount_name, local_error->message);
-              g_clear_error (&local_error);
-              continue;
-            }
-
-          /* There is a valid repo at (or pointed to by)
-           * $mount_root/.ostree/repos/$refs[i]->collection_id/$refs[i]->ref_name.
-           * Add it to the results, keyed by the canonicalised repository URI
-           * to deduplicate the results. */
-
-          g_autofree char *repo_abspath = g_build_filename (mount_root_path, ".ostree/repos",
-                                                            collection_and_ref, NULL);
-          /* FIXME - why are we using realpath here? */
-          g_autofree char *canonical_repo_dir_path = realpath (repo_abspath, NULL);
-          resolved_repo_uri = g_strconcat ("file://", canonical_repo_dir_path, NULL);
-          g_debug ("Resolved ref (%s, %s) on mount ‘%s’ to repo URI ‘%s’ with keyring ‘%s’.",
-                   refs[i]->collection_id, refs[i]->ref_name, mount_name, resolved_repo_uri, keyring);
-
-          resolved_repo = uri_and_keyring_new (resolved_repo_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 */);
-            }
-
-          g_hash_table_insert (supported_ref_to_checksum, (gpointer) refs[i], g_strdup (checksum));
         }
 
       /* Aggregate the results. */
index 8376e51eb900b10d4794eadb1925f0cdf91055d5..2ab080011b73ca537a7343cc3c3434ff85561663 100644 (file)
@@ -70,8 +70,6 @@ static void
 teardown (Fixture       *fixture,
           gconstpointer  test_data)
 {
-  g_autoptr(GError) error = NULL;
-
   /* Recursively remove the temporary directory. */
   (void)glnx_tmpdir_delete (&fixture->tmpdir, NULL, NULL);
 
@@ -150,7 +148,7 @@ test_repo_finder_mount_no_mounts (Fixture       *fixture,
   g_main_context_pop_thread_default (context);
 }
 
-/* Create a .ostree/repos directory under the given @mount_root, or abort. */
+/* Create a .ostree/repos.d directory under the given @mount_root, or abort. */
 static gboolean
 assert_create_repos_dir (Fixture      *fixture,
                          const gchar  *mount_root_name,
@@ -160,7 +158,7 @@ assert_create_repos_dir (Fixture      *fixture,
   glnx_fd_close int repos_dfd = -1;
   g_autoptr(GError) error = NULL;
 
-  g_autofree gchar *path = g_build_filename (mount_root_name, ".ostree", "repos", NULL);
+  g_autofree gchar *path = g_build_filename (mount_root_name, ".ostree", "repos.d", NULL);
   glnx_shutil_mkdir_p_at_open (fixture->tmpdir.fd, path, 0700, &repos_dfd, NULL, &error);
   if (g_error_matches (error, G_IO_ERROR, G_IO_ERROR_EXISTS))
     g_clear_error (&error);
@@ -227,22 +225,22 @@ assert_create_remote_va (Fixture *fixture,
 }
 
 static OstreeRepo *
-assert_create_repo_dir (Fixture                    *fixture,
-                        int                         repos_dfd,
-                        GMount                     *repos_mount,
-                        const OstreeCollectionRef  *ref,
-                        gchar                     **out_uri,
+assert_create_repo_dir (Fixture     *fixture,
+                        int          repos_dfd,
+                        GMount      *repos_mount,
+                        const char  *repo_name,
+                        gchar      **out_uri,
                         ...) G_GNUC_NULL_TERMINATED;
 
-/* Create a @ref directory under the given @repos_dfd, or abort. Create a new
- * repository in it with the refs given in @..., as per assert_create_remote_va().
- * Return the URI of the repository. */
+/* Create a @repo_name directory under the given @repos_dfd, or abort. Create a
+ * new repository in it with the refs given in @..., as per
+ * assert_create_remote_va(). Return the URI of the repository. */
 static OstreeRepo *
-assert_create_repo_dir (Fixture                    *fixture,
-                        int                         repos_dfd,
-                        GMount                     *repos_mount,
-                        const OstreeCollectionRef  *ref,
-                        gchar                     **out_uri,
+assert_create_repo_dir (Fixture     *fixture,
+                        int          repos_dfd,
+                        GMount      *repos_mount,
+                        const char  *repo_name,
+                        gchar      **out_uri,
                         ...)
 {
   glnx_fd_close int ref_dfd = -1;
@@ -250,15 +248,14 @@ assert_create_repo_dir (Fixture                    *fixture,
   g_autoptr(GError) error = NULL;
   va_list args;
 
-  g_autofree gchar *path = g_build_filename (ref->collection_id, ref->ref_name, NULL);
-  glnx_shutil_mkdir_p_at_open (repos_dfd, path, 0700, &ref_dfd, NULL, &error);
+  glnx_shutil_mkdir_p_at_open (repos_dfd, repo_name, 0700, &ref_dfd, NULL, &error);
   if (g_error_matches (error, G_IO_ERROR, G_IO_ERROR_EXISTS))
     g_clear_error (&error);
   g_assert_no_error (error);
 
   g_autoptr(GFile) mount_root = g_mount_get_root (repos_mount);
-  g_autoptr(GFile) repos_dir = g_file_get_child (mount_root, ".ostree/repos");
-  g_autoptr(GFile) repo_dir = g_file_get_child (repos_dir, path);
+  g_autoptr(GFile) repos_dir = g_file_get_child (mount_root, ".ostree/repos.d");
+  g_autoptr(GFile) repo_dir = g_file_get_child (repos_dir, repo_name);
 
   va_start (args, out_uri);
   repo = assert_create_remote_va (fixture, repo_dir, args);
@@ -269,38 +266,19 @@ assert_create_repo_dir (Fixture                    *fixture,
   return g_steal_pointer (&repo);
 }
 
-/* Create a @ref symlink under the given @repos_dfd, pointing to
- * @symlink_target, or abort. */
-static int
-assert_create_repo_symlink (int                        repos_dfd,
-                            const OstreeCollectionRef *ref,
-                            const gchar               *symlink_target_path)
+/* Create a @repo_name symlink under the given @repos_dfd, pointing to
+ * @symlink_target_path, or abort. */
+static void
+assert_create_repo_symlink (int         repos_dfd,
+                            const char *repo_name,
+                            const char *symlink_target_path)
 {
-  glnx_fd_close int symlink_target_dfd = -1;
-  g_autoptr(GError) error = NULL;
-
-  /* The @ref_parent_dir is not necessarily @collection_dir, since @ref may
-   * contain slashes. */
-  g_autofree gchar *path = g_build_filename (ref->collection_id, ref->ref_name, NULL);
-  g_autofree gchar *path_parent = g_path_get_dirname (path);
-
-  glnx_shutil_mkdir_p_at (repos_dfd, path_parent, 0700, NULL, &error);
-  if (g_error_matches (error, G_IO_ERROR, G_IO_ERROR_EXISTS))
-    g_clear_error (&error);
-  g_assert_no_error (error);
-
-  if (TEMP_FAILURE_RETRY (symlinkat (symlink_target_path, repos_dfd, path)) != 0)
+  if (TEMP_FAILURE_RETRY (symlinkat (symlink_target_path, repos_dfd, repo_name)) != 0)
     {
       g_autoptr(GError) error = NULL;
       glnx_throw_errno_prefix (&error, "symlinkat");
       g_assert_no_error (error);
     }
-
-  /* Return a dir FD for the symlink target. */
-  glnx_opendirat (repos_dfd, path, TRUE, &symlink_target_dfd, &error);
-  g_assert_no_error (error);
-
-  return glnx_steal_fd (&symlink_target_dfd);
 }
 
 /* Add configuration for a remote named @remote_name, at @remote_uri, with a
@@ -350,7 +328,7 @@ test_repo_finder_mount_mixed_mounts (Fixture       *fixture,
   g_autofree gchar *repo2_repo_a_uri = NULL;
   g_autofree gchar *repo1_ref0_checksum = NULL, *repo1_ref1_checksum = NULL, *repo1_ref2_checksum = NULL;
   g_autofree gchar *repo2_ref0_checksum = NULL, *repo2_ref1_checksum = NULL, *repo2_ref2_checksum = NULL;
-  g_autofree gchar *repo1_ref5_checksum = NULL;
+  g_autofree gchar *repo1_ref5_checksum = NULL, *repo2_ref3_checksum = NULL;
   gsize i;
   const OstreeCollectionRef ref0 = { "org.example.Collection1", "exampleos/x86_64/ref0" };
   const OstreeCollectionRef ref1 = { "org.example.Collection1", "exampleos/x86_64/ref1" };
@@ -373,27 +351,26 @@ test_repo_finder_mount_mixed_mounts (Fixture       *fixture,
   assert_create_repos_dir (fixture, "no-repos-mount", &no_repos_repos, &no_repos_mount);
 
   assert_create_repos_dir (fixture, "repo1-mount", &repo1_repos, &repo1_mount);
-  repo1_repo_a = assert_create_repo_dir (fixture, repo1_repos, repo1_mount, refs[0], &repo1_repo_a_uri,
+  repo1_repo_a = assert_create_repo_dir (fixture, repo1_repos, repo1_mount, "repo1-repo-a", &repo1_repo_a_uri,
                                          refs[0], &repo1_ref0_checksum,
                                          refs[2], &repo1_ref2_checksum,
                                          refs[5], &repo1_ref5_checksum,
                                          NULL);
-  repo1_repo_b = assert_create_repo_dir (fixture, repo1_repos, repo1_mount, refs[1], &repo1_repo_b_uri,
+  repo1_repo_b = assert_create_repo_dir (fixture, repo1_repos, repo1_mount, "repo1-repo-b", &repo1_repo_b_uri,
                                          refs[1], &repo1_ref1_checksum,
                                          NULL);
-  assert_create_repo_symlink (repo1_repos, refs[2], "ref0");  /* repo1_repo_a */
-  assert_create_repo_symlink (repo1_repos, refs[5], "../../../org.example.Collection1/exampleos/x86_64/ref0");  /* repo1_repo_a */
+  assert_create_repo_symlink (repo1_repos, "repo1-repo-a-alias", "repo1-repo-a");
 
   assert_create_repos_dir (fixture, "repo2-mount", &repo2_repos, &repo2_mount);
-  repo2_repo_a = assert_create_repo_dir (fixture, repo2_repos, repo2_mount, refs[0], &repo2_repo_a_uri,
+  repo2_repo_a = assert_create_repo_dir (fixture, repo2_repos, repo2_mount, "repo2-repo-a", &repo2_repo_a_uri,
                                          refs[0], &repo2_ref0_checksum,
                                          refs[1], &repo2_ref1_checksum,
                                          refs[2], &repo2_ref2_checksum,
-                                         refs[3], NULL,
+                                         refs[3], &repo2_ref3_checksum,
                                          NULL);
-  assert_create_repo_symlink (repo2_repos, refs[1], "ref0");  /* repo2_repo_a */
-  assert_create_repo_symlink (repo2_repos, refs[2], "ref1");  /* repo2_repo_b */
-  assert_create_repo_symlink (repo2_repos, refs[3], "/");
+  assert_create_repo_symlink (repo2_repos, "repo2-repo-a-alias", "repo2-repo-a");
+  assert_create_repo_symlink (repo2_repos, "dangling-symlink", "repo2-repo-b");
+  assert_create_repo_symlink (repo2_repos, "root", "/");
 
   mounts = g_list_prepend (mounts, non_removable_mount);
   mounts = g_list_prepend (mounts, no_repos_mount);
@@ -456,10 +433,108 @@ test_repo_finder_mount_mixed_mounts (Fixture       *fixture,
       else if (g_strcmp0 (uri, repo2_repo_a_uri) == 0 &&
                g_strcmp0 (keyring, "remote1.trustedkeys.gpg") == 0)
         {
-          g_assert_cmpuint (g_hash_table_size (result->ref_to_checksum), ==, 3);
+          g_assert_cmpuint (g_hash_table_size (result->ref_to_checksum), ==, 4);
           g_assert_cmpstr (g_hash_table_lookup (result->ref_to_checksum, refs[0]), ==, repo2_ref0_checksum);
           g_assert_cmpstr (g_hash_table_lookup (result->ref_to_checksum, refs[1]), ==, repo2_ref1_checksum);
           g_assert_cmpstr (g_hash_table_lookup (result->ref_to_checksum, refs[2]), ==, repo2_ref2_checksum);
+          g_assert_cmpstr (g_hash_table_lookup (result->ref_to_checksum, refs[3]), ==, repo2_ref3_checksum);
+        }
+      else
+        {
+          g_test_message ("Unknown result ‘%s’ with keyring ‘%s’.",
+                          result->remote->name, result->remote->keyring);
+          g_assert_not_reached ();
+        }
+    }
+
+  g_main_context_pop_thread_default (context);
+}
+
+/* Test resolving the refs against a mock volume which contains two repositories
+ * in the default repository paths ostree/repo and .ostree/repo, to check that
+ * those paths are read */
+static void
+test_repo_finder_mount_well_known (Fixture       *fixture,
+                                   gconstpointer  test_data)
+{
+  g_autoptr(OstreeRepoFinderMount) finder = NULL;
+  g_autoptr(GVolumeMonitor) monitor = NULL;
+  g_autoptr(GMainContext) context = NULL;
+  g_autoptr(GAsyncResult) result = NULL;
+  g_autoptr(GPtrArray) results = NULL;  /* (element-type OstreeRepoFinderResult) */
+  g_autoptr(GError) error = NULL;
+  g_autoptr(GList) mounts = NULL;  /* (element-type OstreeMockMount)  */
+  g_autoptr(GMount) mount = NULL;
+  glnx_fd_close int repos = -1;
+  g_autoptr(OstreeRepo) repo_a = NULL, repo_b = NULL;
+  g_autofree gchar *repo_a_uri = NULL, *repo_b_uri = NULL;
+  g_autofree gchar *ref_a_checksum = NULL, *ref_b_checksum = NULL;
+  gsize i;
+  const OstreeCollectionRef ref_a = { "org.example.Collection1", "refA" };
+  const OstreeCollectionRef ref_b = { "org.example.Collection2", "refB" };
+  const OstreeCollectionRef * const refs[] = { &ref_a, &ref_b, NULL };
+
+  context = g_main_context_new ();
+  g_main_context_push_thread_default (context);
+
+  /* Build the various mock drives/volumes/mounts, and some repositories with
+   * refs within them. We use "/" under the assumption that it’s on a separate
+   * file system from /tmp, so it’s an example of a symlink pointing outside
+   * its mount point. */
+  assert_create_repos_dir (fixture, "mount", &repos, &mount);
+  repo_a = assert_create_repo_dir (fixture, repos, mount, "../../ostree/repo", &repo_a_uri,
+                                   &ref_a, &ref_a_checksum,
+                                   NULL);
+  repo_b = assert_create_repo_dir (fixture, repos, mount, "../../.ostree/repo", &repo_b_uri,
+                                   &ref_b, &ref_b_checksum,
+                                   NULL);
+  assert_create_repo_symlink (repos, "repo-a-alias", "../../ostree/repo");
+
+  mounts = g_list_prepend (mounts, mount);
+
+  monitor = ostree_mock_volume_monitor_new (mounts, NULL);
+  finder = ostree_repo_finder_mount_new (monitor);
+
+  assert_create_remote_config (fixture->parent_repo, "remote1", "https://nope1", "org.example.Collection1");
+  assert_create_remote_config (fixture->parent_repo, "remote2", "https://nope2", "org.example.Collection2");
+
+  /* Resolve the refs. */
+  ostree_repo_finder_resolve_async (OSTREE_REPO_FINDER (finder), refs,
+                                    fixture->parent_repo,
+                                    NULL, result_cb, &result);
+
+  while (result == NULL)
+    g_main_context_iteration (context, TRUE);
+
+  results = ostree_repo_finder_resolve_finish (OSTREE_REPO_FINDER (finder),
+                                               result, &error);
+  g_assert_no_error (error);
+  g_assert_nonnull (results);
+  g_assert_cmpuint (results->len, ==, 2);
+
+  /* Check that the results are correct: the valid results canonicalised and
+   * deduplicated. */
+  for (i = 0; i < results->len; i++)
+    {
+      g_autofree gchar *uri = NULL;
+      const gchar *keyring;
+      const OstreeRepoFinderResult *result = g_ptr_array_index (results, i);
+
+      uri = g_key_file_get_string (result->remote->options, result->remote->group, "url", &error);
+      g_assert_no_error (error);
+      keyring = result->remote->keyring;
+
+      if (g_strcmp0 (uri, repo_a_uri) == 0 &&
+          g_strcmp0 (keyring, "remote1.trustedkeys.gpg") == 0)
+        {
+          g_assert_cmpuint (g_hash_table_size (result->ref_to_checksum), ==, 1);
+          g_assert_cmpstr (g_hash_table_lookup (result->ref_to_checksum, &ref_a), ==, ref_a_checksum);
+        }
+      else if (g_strcmp0 (uri, repo_b_uri) == 0 &&
+          g_strcmp0 (keyring, "remote2.trustedkeys.gpg") == 0)
+        {
+          g_assert_cmpuint (g_hash_table_size (result->ref_to_checksum), ==, 1);
+          g_assert_cmpstr (g_hash_table_lookup (result->ref_to_checksum, &ref_b), ==, ref_b_checksum);
         }
       else
         {
@@ -482,6 +557,8 @@ int main (int argc, char **argv)
               test_repo_finder_mount_no_mounts, teardown);
   g_test_add ("/repo-finder-mount/mixed-mounts", Fixture, NULL, setup,
               test_repo_finder_mount_mixed_mounts, teardown);
+  g_test_add ("/repo-finder-mount/well-known", Fixture, NULL, setup,
+              test_repo_finder_mount_well_known, teardown);
 
   return g_test_run();
 }