Radish alpha
H
rad:z3QDZAW2FAfuLvihrhiyDC9fAD8G9
HardenedBSD Package Manager
Radicle
Git
rwhich: implement file tracking and search for remote repositories
Baptiste Daroussin committed 7 days ago
commit 9b1f2b153b346f0c6c03247eae5f3fcaac58ae3d
parent bc26687
23 files changed +1195 -42
modified docs/pkg-repo.8
@@ -78,13 +78,24 @@ See
.Xr pkg-repository 5
for details.
.Pp
-
.Pa filesite.pkg
+
.Pa files.pkg
is an optional compressed archive containing
-
.Pa filesite.yaml ,
-
a database of all files present in all packages in the repository.
+
.Pa files ,
+
a directory-grouped listing of all files present in all packages in the
+
repository.
It is only generated when the
.Fl l
flag is used.
+
The file uses a compact line-based text format where paths are grouped by
+
directory.
+
When fetched by
+
.Xr pkg-update 8 ,
+
this data enables
+
.Xr pkg-rwhich 8
+
to look up which remote package provides a given file.
+
See
+
.Xr pkg-repository 5
+
for a full description of the format.
.Pp
The compressed archives may also contain cryptographic signatures
when the signing mechanism of
@@ -230,7 +241,7 @@ This is the same as setting the
.Ev PKG_REPO_HASH
environment variable.
.It Fl l , Cm --list-files
-
Generate list of all files in repo as filesite.pkg archive.
+
Generate list of all files in repo as files.pkg archive.
.It Fl m Ar meta-file , Cm --meta-file Ar meta-file
Use the specified file as repository meta file instead of the default settings.
.It Fl o Ar output-dir , Cm --output-dir Ar output-dir
modified docs/pkg-repository.5
@@ -89,10 +89,10 @@ The base name of the compressed data archive
.Pq default: Pa data .
.It Cm filesite
The name of the uncompressed file listing
-
.Pq default: Pa filesite.yaml .
+
.Pq default: Pa files .
.It Cm filesite_archive
The base name of the compressed file listing archive
-
.Pq default: Pa filesite .
+
.Pq default: Pa files .
.It Cm maintainer
Optional maintainer string.
.It Cm source
@@ -125,14 +125,100 @@ An array of expired package entries (if configured).
.It Cm packages
An array of all package manifests.
.El
-
.It Pa filesite.pkg
+
.It Pa files.pkg
(Optional, generated with
.Fl l
flag to
.Xr pkg-repo 8 ) .
A compressed archive containing
-
.Pa filesite.yaml ,
-
a concatenation of the file lists from all packages in the repository.
+
.Pa files ,
+
a directory-grouped listing of all files in all packages in the repository.
+
.Pp
+
The file uses a line-based text format.
+
Since newlines cannot appear in Unix file paths, each line is unambiguous
+
and no encoding is required; bytes pass through as-is.
+
.Pp
+
The file is divided into two sections separated by an empty line:
+
.Bl -enum -compact
+
.It
+
A
+
.Em front-compressed directory dictionary ,
+
using the same technique as
+
.Xr locate 1 .
+
Directories are sorted lexicographically; each line is
+
.Do Ar N Ar suffix Dc
+
where
+
.Ar N
+
is the number of bytes to keep from the previous entry and
+
.Ar suffix
+
is appended to form the full path.
+
The line number (starting at 0) is the directory's index.
+
Because sorted paths share long common prefixes, most of each path
+
is elided.
+
.It
+
.Em Package blocks ,
+
each separated by an empty line.
+
Within each block:
+
.Bl -bullet -compact
+
.It
+
The first line is the package header:
+
.Do name version Dc .
+
.It
+
A line starting with
+
.Ql >
+
is a directory index, selecting the current directory from the
+
dictionary (e.g.\&
+
.Ql >2
+
selects directory index 2).
+
The
+
.Ql >
+
prefix prevents ambiguity with file basenames that happen to be
+
purely numeric.
+
.It
+
All other non-empty lines are file basenames within the current directory.
+
.El
+
.El
+
.Pp
+
Example:
+
.Bd -literal -offset indent
+
0 /usr/local/bin
+
15 lib
+
15 share/man/man1
+

+
bash 5.2.26
+
>0
+
bash
+
bashbug
+
>2
+
bash.1.gz
+

+
curl 8.7.1
+
>0
+
curl
+
>1
+
libcurl.so.4
+
.Ed
+
.Pp
+
In this example, the first directory is
+
.Pa /usr/local/bin
+
(index 0, prefix length 0 = full path).
+
Index 1 keeps 15 bytes
+
.Pq Dq /usr/local/
+
and appends
+
.Dq lib
+
to form
+
.Pa /usr/local/lib .
+
Index 2 likewise keeps the same 15-byte prefix and appends
+
.Dq share/man/man1 .
+
The common prefix is never repeated, significantly reducing the size
+
of the directory dictionary.
+
When a client runs
+
.Xr pkg-update 8 ,
+
the data is fetched and loaded into the local repository database
+
.Pq tables Pa file_dirs No and Pa pkg_files ,
+
enabling
+
.Xr pkg-rwhich 8
+
queries.
.El
.Pp
Compressed archives use the
added docs/pkg-rwhich.8
@@ -0,0 +1,96 @@
+
.\"
+
.\" FreeBSD pkg - a next generation package for the installation and maintenance
+
.\" of non-core utilities.
+
.\"
+
.\" Redistribution and use in source and binary forms, with or without
+
.\" modification, are permitted provided that the following conditions
+
.\" are met:
+
.\" 1. Redistributions of source code must retain the above copyright
+
.\"    notice, this list of conditions and the following disclaimer.
+
.\" 2. Redistributions in binary form must reproduce the above copyright
+
.\"    notice, this list of conditions and the following disclaimer in the
+
.\"    documentation and/or other materials provided with the distribution.
+
.\"
+
.\"
+
.\"     @(#)pkg.8
+
.\"
+
.Dd March 21, 2025
+
.Dt PKG-RWHICH 8
+
.Os
+
.Sh NAME
+
.Nm "pkg rwhich"
+
.Nd display which remote package provides a specific file
+
.Sh SYNOPSIS
+
.Nm
+
.Op Fl gq
+
.Op Fl r Ar reponame
+
.Ar file
+
.Sh DESCRIPTION
+
.Nm
+
queries the repository catalogues to find which remote package provides the
+
specified
+
.Ar file .
+
.Pp
+
This requires that the repository was created with the
+
.Fl l
+
flag to
+
.Xr pkg-repo 8
+
.Pq to generate the file listing ,
+
and that the client has run
+
.Xr pkg-update 8
+
to fetch the file metadata.
+
.Pp
+
Unlike
+
.Xr pkg-which 8 ,
+
which searches locally installed packages,
+
.Nm
+
searches the remote repository catalogues.
+
.Sh OPTIONS
+
The following options are supported by
+
.Nm :
+
.Bl -tag -width repository
+
.It Fl g , Cm --glob
+
Treat
+
.Ar file
+
as a glob pattern.
+
.It Fl q , Cm --quiet
+
Be quiet.
+
Only print the package name and version, one per line.
+
.It Fl r Ar reponame , Cm --repository Ar reponame
+
Limit the search to the named repository.
+
.El
+
.Sh EXAMPLES
+
Find which package provides a specific binary:
+
.Bd -literal -offset indent
+
$ pkg rwhich /usr/local/bin/bash
+
/usr/local/bin/bash is provided by package bash-5.2.26_2
+
.Ed
+
.Pp
+
Find which package provides a shared library using a glob pattern:
+
.Bd -literal -offset indent
+
$ pkg rwhich -g '*/lib/libcurl*'
+
*/lib/libcurl* is provided by package curl-8.7.1
+
.Ed
+
.Pp
+
Quiet mode for scripting:
+
.Bd -literal -offset indent
+
$ pkg rwhich -q /usr/local/bin/python3.11
+
python311-3.11.9
+
.Ed
+
.Sh ENVIRONMENT
+
The following environment variables affect the execution of
+
.Nm .
+
See
+
.Xr pkg.conf 5
+
for further description.
+
.Bl -tag -width ".Ev NO_DESCRIPTIONS"
+
.It Ev PKG_DBDIR
+
.It Ev REPOS_DIR
+
.El
+
.Sh SEE ALSO
+
.Xr pkg.conf 5 ,
+
.Xr pkg-repository 5 ,
+
.Xr pkg 8 ,
+
.Xr pkg-repo 8 ,
+
.Xr pkg-update 8 ,
+
.Xr pkg-which 8
modified libpkg/pkg.h.in
@@ -965,6 +965,8 @@ struct pkgdb_it * pkgdb_all_search(struct pkgdb *db, const char *pattern,
    match_t type, pkgdb_field field, pkgdb_field sort, const char *reponame);
struct pkgdb_it * pkgdb_all_search2(struct pkgdb *db, const char *pattern,
    match_t type, pkgdb_field field, pkgdb_field sort, c_charv_t *reponames);
+
struct pkgdb_it *pkgdb_repo_which(struct pkgdb *db, const char *path,
+
    bool glob, c_charv_t *repos);

/**
 * @todo Return directly the struct pkg?
modified libpkg/pkg_manifest.c
@@ -892,21 +892,21 @@ pkg_parse_manifest_file(struct pkg *pkg, const char *file)
	} while (0)

int
-
pkg_emit_filelist(struct pkg *pkg, FILE *f)
+
pkg_emit_filelist(struct pkg *pkg, FILE *f, pkghash **dirs, int *ndirs)
{
-
	ucl_object_t *obj = NULL, *seq;
	struct pkg_file *file = NULL;
-
	xstring *b = NULL;
+
	int cur_idx = -1;

-
	obj = ucl_object_typed_new(UCL_OBJECT);
-
	MANIFEST_EXPORT_FIELD(obj, pkg, origin, string);
-
	MANIFEST_EXPORT_FIELD(obj, pkg, name, string);
-
	MANIFEST_EXPORT_FIELD(obj, pkg, version, string);
+
	fprintf(f, "%s %s\n", pkg->name, pkg->version);

-
	seq = NULL;
	while (pkg_files(pkg, &file) == EPKG_OK) {
		char dpath[MAXPATHLEN];
		const char *dp = file->path;
+
		const char *last_slash, *base;
+
		size_t dir_len;
+
		char dirbuf[MAXPATHLEN];
+
		int idx;
+
		pkghash_entry *e;

		if (pkg->oprefix != NULL) {
			size_t l = strlen(pkg->prefix);
@@ -917,18 +917,42 @@ pkg_emit_filelist(struct pkg *pkg, FILE *f)
				dp = dpath;
			}
		}
-
		urlencode(dp, &b);
-
		if (seq == NULL)
-
			seq = ucl_object_typed_new(UCL_ARRAY);
-
		ucl_array_append(seq, ucl_object_fromlstring(b->buf, strlen(b->buf)));
-
	}
-
	if (seq != NULL)
-
		ucl_object_insert_key(obj, seq, "files", 5, false);

-
	ucl_object_emit_file(obj, UCL_EMIT_JSON_COMPACT, f);
+
		last_slash = strrchr(dp, '/');
+
		if (last_slash != NULL) {
+
			dir_len = last_slash - dp;
+
			if (dir_len == 0)
+
				dir_len = 1;
+
			base = last_slash + 1;
+
		} else {
+
			dir_len = 0;
+
			base = dp;
+
		}

-
	xstring_free(b);
-
	ucl_object_unref(obj);
+
		if (dir_len >= sizeof(dirbuf))
+
			continue;
+
		memcpy(dirbuf, dp, dir_len);
+
		dirbuf[dir_len] = '\0';
+

+
		/* Get or assign directory index */
+
		e = pkghash_get(*dirs, dirbuf);
+
		if (e != NULL) {
+
			idx = (int)(intptr_t)e->value;
+
		} else {
+
			idx = (*ndirs)++;
+
			if (*dirs == NULL)
+
				*dirs = pkghash_new();
+
			pkghash_add(*dirs, dirbuf, (void *)(intptr_t)idx, NULL);
+
		}
+

+
		if (idx != cur_idx) {
+
			fprintf(f, ">%d\n", idx);
+
			cur_idx = idx;
+
		}
+
		fprintf(f, "%s\n", base);
+
	}
+

+
	fprintf(f, "\n");

	return (EPKG_OK);
}
modified libpkg/pkg_repo.c
@@ -798,6 +798,55 @@ pkg_repo_fetch_remote_extract_fd(struct pkg_repo *repo, struct pkg_repo_content
	    &prc->manifest_fd, &prc->manifest_len));
}

+
int
+
pkg_repo_fetch_filesite_fd(struct pkg_repo *repo, struct pkg_repo_content *prc)
+
{
+
	int fd;
+
	const char *tmpdir;
+
	char tmp[MAXPATHLEN];
+
	struct stat st;
+
	int rc = EPKG_OK;
+
	const char *name = repo->meta->filesite;
+

+
	/* Try to fetch filesite silently — it is optional */
+
	fd = pkg_repo_fetch_remote_tmp(repo, name, "pkg", &prc->mtime, &rc, true);
+
	if (fd == -1) {
+
		fd = pkg_repo_fetch_remote_tmp(repo, name,
+
		    packing_format_to_string(repo->meta->packing_format),
+
		    &prc->mtime, &rc, true);
+
		if (fd == -1)
+
			return (EPKG_FATAL);
+
	}
+

+
	tmpdir = getenv("TMPDIR");
+
	if (tmpdir == NULL)
+
		tmpdir = "/tmp";
+
	snprintf(tmp, sizeof(tmp), "%s/%s.XXXXXX", tmpdir, name);
+
	prc->filesite_fd = mkstemp(tmp);
+
	if (prc->filesite_fd == -1) {
+
		close(fd);
+
		return (EPKG_FATAL);
+
	}
+

+
	unlink(tmp);
+
	if (pkg_repo_archive_extract_check_archive(fd, name, repo,
+
	    prc->filesite_fd) != EPKG_OK) {
+
		close(prc->filesite_fd);
+
		prc->filesite_fd = -1;
+
		close(fd);
+
		return (EPKG_FATAL);
+
	}
+

+
	close(fd);
+
	if (fstat(prc->filesite_fd, &st) == -1) {
+
		close(prc->filesite_fd);
+
		prc->filesite_fd = -1;
+
		return (EPKG_FATAL);
+
	}
+

+
	return (EPKG_OK);
+
}
+

struct pkg_repo_check_cbdata {
	unsigned char *map;
	size_t len;
modified libpkg/pkg_repo_create.c
@@ -294,6 +294,8 @@ struct thr_env {
	pthread_mutex_t llock;
	pthread_mutex_t flock;
	pthread_cond_t cond;
+
	pkghash *file_dirs;
+
	int nfile_dirs;
};

static void *
@@ -350,7 +352,8 @@ pkg_create_repo_thread(void *arg)
			ucl_object_unref(o);

			if (te->ffile != NULL) {
-
				pkg_emit_filelist(pkg, te->ffile);
+
				pkg_emit_filelist(pkg, te->ffile,
+
				    &te->file_dirs, &te->nfile_dirs);
			}

			pthread_mutex_unlock(&te->flock);
@@ -704,6 +707,19 @@ cleanup:
	return (ret);
}

+
struct dir_entry {
+
	char *path;
+
	int old_idx;
+
};
+

+
static int
+
dir_entry_cmp(const void *a, const void *b)
+
{
+
	const struct dir_entry *da = a;
+
	const struct dir_entry *db = b;
+
	return (strcmp(da->path, db->path));
+
}
+

int
pkg_repo_create(struct pkg_repo_create *prc, char *path)
{
@@ -721,7 +737,7 @@ pkg_repo_create(struct pkg_repo_create *prc, char *path)
	if (prc->outdir == NULL)
		prc->outdir = path;

-
	te.dfile = te.ffile = te.mfile = NULL;
+
	te.dfile = te.mfile = NULL;

	if (!is_dir(path)) {
		pkg_emit_error("%s is not a directory", path);
@@ -806,13 +822,11 @@ pkg_repo_create(struct pkg_repo_create *prc, char *path)
	}

	if (prc->filelist) {
-
		if ((ffd = openat(prc->ofd, prc->meta->filesite,
-
		        O_CREAT|O_TRUNC|O_WRONLY, 00644)) == -1) {
-
			goto cleanup;
-
		}
-
		if ((te.ffile = fdopen(ffd,"w")) == NULL) {
+
		te.ffile = tmpfile();
+
		if (te.ffile == NULL)
			goto cleanup;
-
		}
+
		te.file_dirs = NULL;
+
		te.nfile_dirs = 0;
	}

	len = 0;
@@ -855,8 +869,8 @@ pkg_repo_create(struct pkg_repo_create *prc, char *path)
	ucl_object_emit_streamline_start_container(te.ctx, ar);

	for (int i = 0; i < num_workers; i++) {
-
		/* Create new worker */
-
		pthread_create(&threads[i], NULL, &pkg_create_repo_thread, &te);
+
		pthread_create(&threads[i], NULL,
+
		    &pkg_create_repo_thread, &te);
	}

	pthread_mutex_lock(&te.nlock);
@@ -869,6 +883,92 @@ pkg_repo_create(struct pkg_repo_create *prc, char *path)
	for (int i = 0; i < num_workers; i++)
		pthread_join(threads[i], NULL);
	free(threads);
+

+
	/* Assemble filelist: write dictionary header + package data */
+
	if (te.ffile != NULL && te.file_dirs != NULL) {
+
		FILE *final;
+
		char *line = NULL;
+
		size_t linecap = 0;
+
		ssize_t linelen;
+

+
		ffd = openat(prc->ofd, prc->meta->filesite,
+
		    O_CREAT|O_TRUNC|O_WRONLY, 00644);
+
		if (ffd != -1 && (final = fdopen(ffd, "w")) != NULL) {
+
			/*
+
			 * Sort directories and write a front-compressed
+
			 * dictionary.  Each line is "N suffix" where N
+
			 * is the number of bytes to keep from the
+
			 * previous path.  This is the same technique
+
			 * used by locate(1).
+
			 */
+
			struct dir_entry *dirs = xcalloc(te.nfile_dirs,
+
			    sizeof(struct dir_entry));
+
			pkghash_it hit = pkghash_iterator(te.file_dirs);
+
			while (pkghash_next(&hit)) {
+
				int idx = (int)(intptr_t)hit.value;
+
				dirs[idx].path = hit.key;
+
				dirs[idx].old_idx = idx;
+
			}
+
			qsort(dirs, te.nfile_dirs,
+
			    sizeof(struct dir_entry), dir_entry_cmp);
+

+
			int *old_to_new = xcalloc(te.nfile_dirs,
+
			    sizeof(int));
+
			for (int i = 0; i < te.nfile_dirs; i++)
+
				old_to_new[dirs[i].old_idx] = i;
+

+
			const char *prev = "";
+
			for (int i = 0; i < te.nfile_dirs; i++) {
+
				const char *cur = dirs[i].path;
+
				int common = 0;
+
				while (prev[common] != '\0' &&
+
				    cur[common] != '\0' &&
+
				    prev[common] == cur[common])
+
					common++;
+
				fprintf(final, "%d %s\n", common,
+
				    cur + common);
+
				prev = cur;
+
			}
+

+
			fprintf(final, "\n");
+

+
			/* Append package data, remapping directory indices */
+
			fflush(te.ffile);
+
			rewind(te.ffile);
+
			while ((linelen = getline(&line, &linecap,
+
			    te.ffile)) > 0) {
+
				if (linelen > 0 &&
+
				    line[linelen - 1] == '\n')
+
					line[--linelen] = '\0';
+
				if (linelen == 0) {
+
					fprintf(final, "\n");
+
					continue;
+
				}
+
				if (line[0] == '>') {
+
					long idx = strtol(line + 1,
+
					    NULL, 10);
+
					if (idx >= 0 &&
+
					    idx < te.nfile_dirs)
+
						fprintf(final, ">%d\n",
+
						    old_to_new[idx]);
+
					else
+
						fprintf(final, "%s\n",
+
						    line);
+
				} else {
+
					fprintf(final, "%s\n", line);
+
				}
+
			}
+

+
			free(old_to_new);
+
			free(dirs);
+
			free(line);
+

+
			fclose(final);
+
		}
+
		pkghash_destroy(te.file_dirs);
+
		te.file_dirs = NULL;
+
	}
+

	ucl_object_emit_streamline_end_container(te.ctx);
	pkg_emit_progress_tick(len, len);
	ucl_object_emit_streamline_finish(te.ctx);
modified libpkg/pkg_repo_meta.c
@@ -51,8 +51,8 @@ pkg_repo_meta_set_default(struct pkg_repo_meta *meta)
	meta->data_archive = xstrdup("data");
	meta->manifests = xstrdup("packagesite.yaml");
	meta->manifests_archive = xstrdup("packagesite");
-
	meta->filesite = xstrdup("filesite.yaml");
-
	meta->filesite_archive = xstrdup("filesite");
+
	meta->filesite = xstrdup("files");
+
	meta->filesite_archive = xstrdup("files");
	/* Not using fulldb */
	meta->fulldb = NULL;
	meta->fulldb_archive = NULL;
modified libpkg/pkgdb_query.c
@@ -497,6 +497,27 @@ pkgdb_repo_provide(struct pkgdb *db, const char *require, c_charv_t *repo)
}

struct pkgdb_it *
+
pkgdb_repo_which(struct pkgdb *db, const char *path, bool glob, c_charv_t *repos)
+
{
+
	struct pkgdb_it *it;
+
	struct pkg_repo_it *rit;
+

+
	it = pkgdb_it_new_repo(db);
+

+
	vec_foreach(db->repos, i) {
+
		if (consider_this_repo(repos, db->repos.d[i]->name)) {
+
			if (db->repos.d[i]->ops->file_which != NULL) {
+
				rit = db->repos.d[i]->ops->file_which(db->repos.d[i], path, glob);
+
				if (rit != NULL)
+
					pkgdb_it_repo_attach(it, rit);
+
			}
+
		}
+
	}
+

+
	return (it);
+
}
+

+
struct pkgdb_it *
pkgdb_repo_search(struct pkgdb *db, const char *pattern, match_t match,
    pkgdb_field field, pkgdb_field sort, const char *repo)
{
modified libpkg/private/pkg.h
@@ -167,6 +167,7 @@ struct pkg_repo_content {
	int manifest_fd;
	size_t manifest_len;
	int data_fd;
+
	int filesite_fd;
};

struct pkgsign_ctx;
@@ -532,6 +533,8 @@ struct pkg_repo_ops {
					const char *);
	struct pkg_repo_it * (*provided)(struct pkg_repo *,
					const char *);
+
	struct pkg_repo_it * (*file_which)(struct pkg_repo *,
+
					const char *, bool);
	struct pkg_repo_it * (*search)(struct pkg_repo *, const char *, match_t,
					pkgdb_field field, pkgdb_field sort);
	struct pkg_repo_it * (*groupsearch)(struct pkg_repo *, const char *,
@@ -728,6 +731,7 @@ int pkg_repo_meta_dump_fd(struct pkg_repo_meta *target, const int fd);
int pkg_repo_fetch_meta(struct pkg_repo *repo, time_t *t);
int pkg_repo_fetch_remote_extract_fd(struct pkg_repo *repo, struct pkg_repo_content *);
int pkg_repo_fetch_data_fd(struct pkg_repo *repo, struct pkg_repo_content *);
+
int pkg_repo_fetch_filesite_fd(struct pkg_repo *repo, struct pkg_repo_content *);

struct pkg_repo_meta *pkg_repo_meta_default(void);
int pkg_repo_meta_load(const int fd, struct pkg_repo_meta **target);
@@ -817,7 +821,7 @@ int pkgdb_set_pkg_digest(struct pkgdb *db, struct pkg *pkg);
int pkgdb_is_dir_used(struct pkgdb *db, struct pkg *p, const char *dir, int64_t *res);
int pkgdb_file_set_cksum(struct pkgdb *db, struct pkg_file *file, const char *sha256);

-
int pkg_emit_filelist(struct pkg *, FILE *);
+
int pkg_emit_filelist(struct pkg *, FILE *, pkghash **dirs, int *ndirs);

bool ucl_object_emit_buf(const ucl_object_t *obj, enum ucl_emitter emit_type,
    xstring **buf);
modified libpkg/private/pkgdb.h
@@ -108,6 +108,8 @@ struct pkgdb_it *pkgdb_repo_provide(struct pkgdb *db, const char *require, c_cha

struct pkgdb_it *pkgdb_repo_require(struct pkgdb *db, const char *provide, c_charv_t *repo);

+
struct pkgdb_it *pkgdb_repo_which(struct pkgdb *db, const char *path, bool glob, c_charv_t *repos);
+

/**
 * Unregister a package from the database
 * @return An error code.
modified libpkg/repo/binary/binary.c
@@ -38,6 +38,7 @@ struct pkg_repo_ops pkg_repo_binary_ops = {
	.shlib_required = pkg_repo_binary_shlib_require,
	.provided = pkg_repo_binary_provide,
	.required = pkg_repo_binary_require,
+
	.file_which = pkg_repo_binary_file_which,
	.search = pkg_repo_binary_search,
	.groupsearch = pkg_repo_binary_groupsearch,
	.fetch_pkg = pkg_repo_binary_fetch,
modified libpkg/repo/binary/binary.h
@@ -52,6 +52,8 @@ struct pkg_repo_it *pkg_repo_binary_shlib_require(struct pkg_repo *repo,
	const char *provide);
struct pkg_repo_it *pkg_repo_binary_require(struct pkg_repo *repo,
	const char *provide);
+
struct pkg_repo_it *pkg_repo_binary_file_which(struct pkg_repo *repo,
+
	const char *path, bool glob);
struct pkg_repo_it *pkg_repo_binary_search(struct pkg_repo *repo,
	const char *pattern, match_t match,
    pkgdb_field field, pkgdb_field sort);
modified libpkg/repo/binary/binary_private.h
@@ -154,6 +154,18 @@ static const char binary_repo_initsql[] = ""
		"  ON DELETE RESTRICT ON UPDATE RESTRICT,"
		"UNIQUE(package_id, require_id)"
	");"
+
	"CREATE TABLE file_dirs ("
+
		"id INTEGER PRIMARY KEY,"
+
		"path TEXT NOT NULL UNIQUE"
+
	");"
+
	"CREATE TABLE pkg_files ("
+
		"package_id INTEGER NOT NULL REFERENCES packages(id)"
+
		"  ON DELETE CASCADE ON UPDATE CASCADE,"
+
		"dir_id INTEGER NOT NULL REFERENCES file_dirs(id)"
+
		"  ON DELETE RESTRICT ON UPDATE RESTRICT,"
+
		"name TEXT NOT NULL,"
+
		"UNIQUE(package_id, dir_id, name)"
+
	");"
/*	"CREATE INDEX packages_origin ON packages(origin COLLATE NOCASE);"
	"CREATE INDEX packages_name ON packages(name COLLATE NOCASE);"
	"CREATE INDEX packages_uid_nocase ON packages(name COLLATE NOCASE, origin COLLATE NOCASE);"
@@ -171,7 +183,7 @@ static const char binary_repo_initsql[] = ""
/* The package repo schema minor revision.
   Minor schema changes don't prevent older pkgng
   versions accessing the repo. */
-
#define REPO_SCHEMA_MINOR 14
+
#define REPO_SCHEMA_MINOR 15

#define REPO_SCHEMA_VERSION (REPO_SCHEMA_MAJOR * 1000 + REPO_SCHEMA_MINOR)

@@ -196,6 +208,9 @@ typedef enum _sql_prstmt_index {
	PROVIDES,
	REQUIRE,
	REQUIRES,
+
	FILEDIR1,
+
	FILEDIR2,
+
	PKGID,
	PRSTMT_LAST,
} sql_prstmt_index;

modified libpkg/repo/binary/common.c
@@ -134,6 +134,19 @@ static sql_prstmt sql_prepared_statements[PRSTMT_LAST] = {
		"INSERT OR IGNORE INTO pkg_requires(package_id, require_id) "
		"VALUES (?1, (SELECT id FROM requires WHERE require = ?2))",
	},
+
	[FILEDIR1] = {
+
		NULL,
+
		"INSERT OR IGNORE INTO file_dirs(path) VALUES(?1)",
+
	},
+
	[FILEDIR2] = {
+
		NULL,
+
		"INSERT OR IGNORE INTO pkg_files(package_id, dir_id, name) "
+
		"VALUES (?1, (SELECT id FROM file_dirs WHERE path = ?2), ?3)",
+
	},
+
	[PKGID] = {
+
		NULL,
+
		"SELECT id FROM packages WHERE name = ?1 AND version = ?2",
+
	},
	/* PRSTMT_LAST */
};

modified libpkg/repo/binary/query.c
@@ -365,6 +365,76 @@ pkg_repo_binary_require(struct pkg_repo *repo, const char *provide)
	return (pkg_repo_binary_it_new(repo, stmt, PKGDB_IT_FLAG_ONCE));
}

+
struct pkg_repo_it *
+
pkg_repo_binary_file_which(struct pkg_repo *repo, const char *path, bool glob)
+
{
+
	sqlite3_stmt	*stmt;
+
	sqlite3 *sqlite = PRIV_GET(repo);
+
	char *sql = NULL;
+
	int64_t has_files = 0;
+

+
	/* Check if file_dirs table exists (filesite may not have been loaded) */
+
	if (get_pragma(sqlite,
+
	    "SELECT count(name) FROM sqlite_master "
+
	    "WHERE type='table' AND name='file_dirs';",
+
	    &has_files, false) != EPKG_OK || has_files == 0) {
+
		return (NULL);
+
	}
+

+
	if (glob) {
+
		const char basesql[] = ""
+
		    "SELECT p.id, p.origin, p.name, p.version, p.comment, "
+
		    "p.name as uniqueid, "
+
		    "p.prefix, p.desc, p.arch, p.maintainer, p.www, "
+
		    "p.licenselogic, p.flatsize, p.pkgsize, "
+
		    "p.cksum, p.manifestdigest, p.path AS repopath, '%s' AS dbname "
+
		    "FROM packages AS p "
+
		    "INNER JOIN pkg_files AS pf ON p.id = pf.package_id "
+
		    "INNER JOIN file_dirs AS fd ON pf.dir_id = fd.id "
+
		    "WHERE fd.path || '/' || pf.name GLOB ?1 "
+
		    "GROUP BY p.id;";
+

+
		xasprintf(&sql, basesql, repo->name);
+
		stmt = prepare_sql(sqlite, sql);
+
		free(sql);
+
		if (stmt == NULL)
+
			return (NULL);
+

+
		sqlite3_bind_text(stmt, 1, path, -1, SQLITE_TRANSIENT);
+
	} else {
+
		const char *last_slash = strrchr(path, '/');
+
		if (last_slash == NULL)
+
			return (NULL);
+

+
		const char basesql[] = ""
+
		    "SELECT p.id, p.origin, p.name, p.version, p.comment, "
+
		    "p.name as uniqueid, "
+
		    "p.prefix, p.desc, p.arch, p.maintainer, p.www, "
+
		    "p.licenselogic, p.flatsize, p.pkgsize, "
+
		    "p.cksum, p.manifestdigest, p.path AS repopath, '%s' AS dbname "
+
		    "FROM packages AS p "
+
		    "INNER JOIN pkg_files AS pf ON p.id = pf.package_id "
+
		    "INNER JOIN file_dirs AS fd ON pf.dir_id = fd.id "
+
		    "WHERE fd.path = ?1 AND pf.name = ?2 "
+
		    "GROUP BY p.id;";
+

+
		xasprintf(&sql, basesql, repo->name);
+
		stmt = prepare_sql(sqlite, sql);
+
		free(sql);
+
		if (stmt == NULL)
+
			return (NULL);
+

+
		size_t dir_len = last_slash - path;
+
		if (dir_len == 0) dir_len = 1;
+
		sqlite3_bind_text(stmt, 1, path, dir_len, SQLITE_TRANSIENT);
+
		sqlite3_bind_text(stmt, 2, last_slash + 1, -1, SQLITE_TRANSIENT);
+
	}
+

+
	pkgdb_debug(4, stmt);
+

+
	return (pkg_repo_binary_it_new(repo, stmt, PKGDB_IT_FLAG_ONCE));
+
}
+

static const char *
pkg_repo_binary_search_how(match_t match)
{
modified libpkg/repo/binary/update.c
@@ -573,13 +573,135 @@ dump_json(struct pkg_repo *repo, const char *line, jsmntok_t *tok, const char *d
}

static int
+
pkg_repo_binary_add_from_filelist(FILE *f, sqlite3 *sqlite)
+
{
+
	char *line = NULL;
+
	size_t linecap = 0;
+
	ssize_t linelen;
+
	int64_t package_id = -1;
+
	sqlite3_stmt *stmt;
+
	int rc = EPKG_OK;
+
	int cnt = 0;
+
	int cur_dir_idx = -1;
+
	char **dir_table = NULL;
+
	int dir_count = 0;
+
	int dir_cap = 0;
+

+
	/*
+
	 * Phase 1: Read front-compressed directory dictionary.
+
	 * Each line is "N suffix" where N is the number of bytes
+
	 * to keep from the previous path (locate(1)-style).
+
	 */
+
	char pathbuf[MAXPATHLEN];
+
	pathbuf[0] = '\0';
+

+
	while ((linelen = getline(&line, &linecap, f)) > 0) {
+
		if (linelen > 0 && line[linelen - 1] == '\n')
+
			line[--linelen] = '\0';
+
		if (linelen == 0)
+
			break;
+

+
		char *sp = strchr(line, ' ');
+
		if (sp == NULL)
+
			continue;
+
		*sp = '\0';
+
		int prefix_len = (int)strtol(line, NULL, 10);
+
		if (prefix_len < 0 ||
+
		    (size_t)prefix_len >= sizeof(pathbuf))
+
			prefix_len = 0;
+
		strlcpy(pathbuf + prefix_len, sp + 1,
+
		    sizeof(pathbuf) - prefix_len);
+

+
		if (dir_count >= dir_cap) {
+
			dir_cap = dir_cap == 0 ? 256 : dir_cap * 2;
+
			dir_table = reallocf(dir_table,
+
			    dir_cap * sizeof(char *));
+
		}
+
		dir_table[dir_count++] = xstrdup(pathbuf);
+

+
		sql_arg_t dir_arg[] = { SQL_ARG(pathbuf) };
+
		if (pkg_repo_binary_run_prstatement(FILEDIR1,
+
		    dir_arg, NELEM(dir_arg)) != SQLITE_DONE) {
+
			rc = EPKG_FATAL;
+
			goto cleanup;
+
		}
+
	}
+

+
	/* Phase 2: Read package blocks */
+
	while ((linelen = getline(&line, &linecap, f)) > 0) {
+
		if (linelen > 0 && line[linelen - 1] == '\n')
+
			line[--linelen] = '\0';
+

+
		/* Empty line = end of package block */
+
		if (linelen == 0) {
+
			package_id = -1;
+
			cur_dir_idx = -1;
+
			continue;
+
		}
+

+
		/* If no current package, this is a header */
+
		if (package_id == -1) {
+
			char *sp = strchr(line, ' ');
+
			if (sp == NULL)
+
				continue;
+
			*sp = '\0';
+

+
			stmt = pkg_repo_binary_stmt_prstatement(PKGID);
+
			sqlite3_reset(stmt);
+
			sqlite3_bind_text(stmt, 1, line, -1, SQLITE_STATIC);
+
			sqlite3_bind_text(stmt, 2, sp + 1, -1, SQLITE_STATIC);
+
			if (sqlite3_step(stmt) == SQLITE_ROW)
+
				package_id = sqlite3_column_int64(stmt, 0);
+
			sqlite3_reset(stmt);
+
			continue;
+
		}
+

+
		/* Directory index lines are prefixed with '>' */
+
		if (line[0] == '>') {
+
			long idx = strtol(line + 1, NULL, 10);
+
			if (idx >= 0 && idx < dir_count) {
+
				cur_dir_idx = (int)idx;
+
				continue;
+
			}
+
		}
+

+
		/* Basename line */
+
		if (cur_dir_idx < 0 || cur_dir_idx >= dir_count)
+
			continue;
+

+
		sql_arg_t file_arg[] = {
+
			SQL_ARG(package_id),
+
			SQL_ARG(dir_table[cur_dir_idx]),
+
			SQL_ARG(line),
+
		};
+
		if (pkg_repo_binary_run_prstatement(FILEDIR2,
+
		    file_arg, NELEM(file_arg)) != SQLITE_DONE) {
+
			rc = EPKG_FATAL;
+
			break;
+
		}
+

+
		cnt++;
+
		if ((cnt % 100) == 0)
+
			pkg_emit_progress_tick(cnt, 0);
+
	}
+

+
cleanup:
+
	for (int i = 0; i < dir_count; i++)
+
		free(dir_table[i]);
+
	free(dir_table);
+
	free(line);
+
	pkg_emit_progress_tick(cnt, cnt);
+
	return (rc);
+
}
+

+
static int
pkg_repo_binary_update_proceed(const char *name, struct pkg_repo *repo,
	time_t *mtime, bool force)
{
	int rc = EPKG_FATAL, cancel = 0;
	sqlite3 *sqlite = NULL;
	int cnt = 0;
-
	time_t local_t;
+
	time_t local_t, orig_mtime;
	bool in_trans = false;
	char *path = NULL;
	FILE *f = NULL;
@@ -594,6 +716,8 @@ pkg_repo_binary_update_proceed(const char *name, struct pkg_repo *repo,
	if (force)
		*mtime = 0;

+
	orig_mtime = *mtime;
+

	/* Fetch meta */
	local_t = *mtime;
	if (pkg_repo_fetch_meta(repo, &local_t) == EPKG_FATAL)
@@ -606,6 +730,7 @@ pkg_repo_binary_update_proceed(const char *name, struct pkg_repo *repo,
	prc.mtime = *mtime;
	prc.manifest_len = 0;
	prc.data_fd = -1;
+
	prc.filesite_fd = -1;

	rc = pkg_repo_fetch_data_fd(repo, &prc);
	if (rc == EPKG_UPTODATE)
@@ -742,6 +867,32 @@ pkg_repo_binary_update_proceed(const char *name, struct pkg_repo *repo,
	"CREATE UNIQUE INDEX packages_digest ON packages(manifestdigest);"
	 );

+
	/* Fetch and process filesite (file lists) if available */
+
	if (rc == EPKG_OK) {
+
		struct pkg_repo_content fprc = { .mtime = orig_mtime, .filesite_fd = -1 };
+
		int frc;
+

+
		frc = pkg_repo_fetch_filesite_fd(repo, &fprc);
+
		if (frc == EPKG_OK) {
+
			FILE *ff = fdopen(fprc.filesite_fd, "r");
+
			if (ff != NULL) {
+
				pkg_emit_progress_start("Processing file entries");
+
				rewind(ff);
+
				pkg_repo_binary_add_from_filelist(ff, sqlite);
+
				fclose(ff);
+

+
				sql_exec(sqlite, ""
+
				    "CREATE INDEX pkg_files_package_id ON pkg_files(package_id);"
+
				    "CREATE INDEX pkg_files_dir_name ON pkg_files(dir_id, name);"
+
				    "CREATE INDEX pkg_files_name ON pkg_files(name);"
+
				);
+
			} else {
+
				close(fprc.filesite_fd);
+
			}
+
		}
+
		/* filesite is optional, don't fail if not available */
+
	}
+

cleanup:

	if (in_trans) {
modified src/Makefile.in
@@ -24,6 +24,7 @@ SRCS= add.c \
	repo.c \
	repositories.c \
	rquery.c \
+
	rwhich.c \
	search.c \
	set.c \
	shell.c \
modified src/main.c
@@ -81,6 +81,7 @@ static struct commands {
	{ "repo", "Creates a package repository catalogue", exec_repo, usage_repo},
	{ "repositories", "Show repositories information", exec_repositories, usage_repositories},
	{ "rquery", "Queries information in repository catalogues", exec_rquery, usage_rquery},
+
	{ "rwhich", "Displays which remote package provides a specific file", exec_rwhich, usage_rwhich},
	{ "search", "Performs a search of package repository catalogues", exec_search, usage_search},
	{ "set", "Modifies information about packages in the local database", exec_set, usage_set},
	{ "ssh", "Package server (to be used via ssh)", exec_ssh, usage_ssh},
modified src/pkgcli.h
@@ -177,6 +177,10 @@ void usage_version(void);
int exec_which(int, char **);
void usage_which(void);

+
/* pkg rwhich */
+
int exec_rwhich(int, char **);
+
void usage_rwhich(void);
+

/* pkg ssh */
int exec_ssh(int, char **);
void usage_ssh(void);
added src/rwhich.c
@@ -0,0 +1,114 @@
+
/*-
+
 * Copyright (c) 2025 Baptiste Daroussin <bapt@FreeBSD.org>
+
 *
+
 * SPDX-License-Identifier: BSD-2-Clause
+
 */
+

+
#include <sys/param.h>
+

+
#include <err.h>
+
#include <getopt.h>
+
#include <stdio.h>
+
#include <string.h>
+
#include <unistd.h>
+

+
#include <pkg.h>
+
#include "pkgcli.h"
+

+
void
+
usage_rwhich(void)
+
{
+
	fprintf(stderr, "Usage: pkg rwhich [-gq] [-r reponame] <file>\n\n");
+
	fprintf(stderr, "For more information see 'pkg help rwhich'.\n");
+
}
+

+
int
+
exec_rwhich(int argc, char **argv)
+
{
+
	struct pkgdb	*db = NULL;
+
	struct pkgdb_it	*it = NULL;
+
	struct pkg	*pkg = NULL;
+
	int		 retcode = EXIT_FAILURE;
+
	int		 ch;
+
	bool		 glob = false;
+
	const char	*reponame = NULL;
+
	c_charv_t	 repos = vec_init();
+

+
	struct option longopts[] = {
+
		{ "glob",		no_argument,		NULL,	'g' },
+
		{ "quiet",		no_argument,		NULL,	'q' },
+
		{ "repository",		required_argument,	NULL,	'r' },
+
		{ NULL,			0,			NULL,	0   },
+
	};
+

+
	while ((ch = getopt_long(argc, argv, "+gqr:", longopts, NULL)) != -1) {
+
		switch (ch) {
+
		case 'g':
+
			glob = true;
+
			break;
+
		case 'q':
+
			quiet = true;
+
			break;
+
		case 'r':
+
			reponame = optarg;
+
			break;
+
		default:
+
			usage_rwhich();
+
			return (EXIT_FAILURE);
+
		}
+
	}
+

+
	argc -= optind;
+
	argv += optind;
+

+
	if (argc < 1) {
+
		usage_rwhich();
+
		return (EXIT_FAILURE);
+
	}
+

+
	if (pkgdb_open_all(&db, PKGDB_REMOTE, reponame) != EPKG_OK)
+
		return (EXIT_FAILURE);
+

+
	if (pkgdb_obtain_lock(db, PKGDB_LOCK_READONLY) != EPKG_OK) {
+
		pkgdb_close(db);
+
		warnx("Cannot get a read lock on a database, "
+
		    "it is locked by another process");
+
		return (EXIT_FAILURE);
+
	}
+

+
	if (reponame != NULL)
+
		vec_push(&repos, reponame);
+

+
	while (argc >= 1) {
+
		if ((it = pkgdb_repo_which(db, argv[0], glob, &repos)) == NULL) {
+
			retcode = EXIT_FAILURE;
+
			goto cleanup;
+
		}
+

+
		pkg = NULL;
+
		while (pkgdb_it_next(it, &pkg, PKG_LOAD_BASIC) == EPKG_OK) {
+
			retcode = EXIT_SUCCESS;
+
			if (quiet)
+
				pkg_printf("%n-%v\n", pkg, pkg);
+
			else
+
				pkg_printf("%S is provided by package %n-%v\n",
+
				    argv[0], pkg, pkg);
+
		}
+
		if (retcode != EXIT_SUCCESS && !quiet)
+
			printf("%s was not found in the repository catalogue\n",
+
			    argv[0]);
+

+
		pkg_free(pkg);
+
		pkgdb_it_free(it);
+

+
		argc--;
+
		argv++;
+
	}
+

+
cleanup:
+
	vec_free(&repos);
+
	pkgdb_release_lock(db, PKGDB_LOCK_READONLY);
+
	pkgdb_close(db);
+

+
	return (retcode);
+
}
modified tests/Makefile.in
@@ -56,6 +56,7 @@ TESTS_SH= \
	frontend/repo.sh \
	frontend/rquery.sh \
	frontend/requires.sh \
+
	frontend/rwhich.sh \
	frontend/rootdir.sh \
	frontend/rubypuppet.sh \
	frontend/search.sh \
added tests/frontend/rwhich.sh
@@ -0,0 +1,385 @@
+
#! /usr/bin/env atf-sh
+

+
. $(atf_get_srcdir)/test_environment.sh
+

+
tests_init \
+
	rwhich_basic \
+
	rwhich_glob \
+
	rwhich_quiet \
+
	rwhich_not_found \
+
	rwhich_multiple_packages \
+
	rwhich_no_args \
+
	rwhich_multiple_args \
+
	rwhich_repo_flag \
+
	rwhich_shared_dirs \
+
	rwhich_no_filelist
+

+
rwhich_basic_body() {
+
	mkdir -p usr/local/bin usr/local/lib
+
	touch usr/local/bin/mybin usr/local/lib/libtest.so.1
+

+
	atf_check -s exit:0 sh ${RESOURCEDIR}/test_subr.sh new_pkg test test 1 "${TMPDIR}"
+
	cat << EOF >> test.ucl
+
files: {
+
    ${TMPDIR}/usr/local/bin/mybin: "",
+
    ${TMPDIR}/usr/local/lib/libtest.so.1: "",
+
}
+
EOF
+

+
	atf_check -s exit:0 pkg create -M test.ucl
+
	atf_check -o ignore -e empty -s exit:0 pkg repo -l .
+

+
	mkdir reposconf
+
	cat << EOF > reposconf/repo.conf
+
local: {
+
	url: file:///${TMPDIR},
+
	enabled: true
+
}
+
EOF
+

+
	atf_check -o ignore -e empty -s exit:0 \
+
		pkg -o REPOS_DIR="${TMPDIR}/reposconf" \
+
		    -o PKG_CACHEDIR="${TMPDIR}" update
+

+
	atf_check \
+
		-o match:"is provided by package test-1" \
+
		-s exit:0 \
+
		pkg -o REPOS_DIR="${TMPDIR}/reposconf" \
+
		    -o PKG_CACHEDIR="${TMPDIR}" rwhich ${TMPDIR}/usr/local/bin/mybin
+

+
	atf_check \
+
		-o match:"is provided by package test-1" \
+
		-s exit:0 \
+
		pkg -o REPOS_DIR="${TMPDIR}/reposconf" \
+
		    -o PKG_CACHEDIR="${TMPDIR}" rwhich ${TMPDIR}/usr/local/lib/libtest.so.1
+
}
+

+
rwhich_glob_body() {
+
	mkdir -p usr/local/bin usr/local/lib
+
	touch usr/local/bin/mybin usr/local/bin/mybin2 usr/local/lib/libtest.so.1
+

+
	atf_check -s exit:0 sh ${RESOURCEDIR}/test_subr.sh new_pkg test test 1 "${TMPDIR}"
+
	cat << EOF >> test.ucl
+
files: {
+
    ${TMPDIR}/usr/local/bin/mybin: "",
+
    ${TMPDIR}/usr/local/bin/mybin2: "",
+
    ${TMPDIR}/usr/local/lib/libtest.so.1: "",
+
}
+
EOF
+

+
	atf_check -s exit:0 pkg create -M test.ucl
+
	atf_check -o ignore -e empty -s exit:0 pkg repo -l .
+

+
	mkdir reposconf
+
	cat << EOF > reposconf/repo.conf
+
local: {
+
	url: file:///${TMPDIR},
+
	enabled: true
+
}
+
EOF
+

+
	atf_check -o ignore -e empty -s exit:0 \
+
		pkg -o REPOS_DIR="${TMPDIR}/reposconf" \
+
		    -o PKG_CACHEDIR="${TMPDIR}" update
+

+
	atf_check \
+
		-o match:"is provided by package test-1" \
+
		-s exit:0 \
+
		pkg -o REPOS_DIR="${TMPDIR}/reposconf" \
+
		    -o PKG_CACHEDIR="${TMPDIR}" rwhich -g "*/lib/libtest*"
+
}
+

+
rwhich_quiet_body() {
+
	mkdir -p usr/local/bin
+
	touch usr/local/bin/mybin
+

+
	atf_check -s exit:0 sh ${RESOURCEDIR}/test_subr.sh new_pkg test test 1 "${TMPDIR}"
+
	cat << EOF >> test.ucl
+
files: {
+
    ${TMPDIR}/usr/local/bin/mybin: "",
+
}
+
EOF
+

+
	atf_check -s exit:0 pkg create -M test.ucl
+
	atf_check -o ignore -e empty -s exit:0 pkg repo -l .
+

+
	mkdir reposconf
+
	cat << EOF > reposconf/repo.conf
+
local: {
+
	url: file:///${TMPDIR},
+
	enabled: true
+
}
+
EOF
+

+
	atf_check -o ignore -e empty -s exit:0 \
+
		pkg -o REPOS_DIR="${TMPDIR}/reposconf" \
+
		    -o PKG_CACHEDIR="${TMPDIR}" update
+

+
	atf_check \
+
		-o inline:"test-1\n" \
+
		-s exit:0 \
+
		pkg -o REPOS_DIR="${TMPDIR}/reposconf" \
+
		    -o PKG_CACHEDIR="${TMPDIR}" rwhich -q ${TMPDIR}/usr/local/bin/mybin
+
}
+

+
rwhich_not_found_body() {
+
	mkdir -p usr/local/bin
+
	touch usr/local/bin/mybin
+

+
	atf_check -s exit:0 sh ${RESOURCEDIR}/test_subr.sh new_pkg test test 1 "${TMPDIR}"
+
	cat << EOF >> test.ucl
+
files: {
+
    ${TMPDIR}/usr/local/bin/mybin: "",
+
}
+
EOF
+

+
	atf_check -s exit:0 pkg create -M test.ucl
+
	atf_check -o ignore -e empty -s exit:0 pkg repo -l .
+

+
	mkdir reposconf
+
	cat << EOF > reposconf/repo.conf
+
local: {
+
	url: file:///${TMPDIR},
+
	enabled: true
+
}
+
EOF
+

+
	atf_check -o ignore -e empty -s exit:0 \
+
		pkg -o REPOS_DIR="${TMPDIR}/reposconf" \
+
		    -o PKG_CACHEDIR="${TMPDIR}" update
+

+
	atf_check \
+
		-o match:"was not found in the repository" \
+
		-s exit:1 \
+
		pkg -o REPOS_DIR="${TMPDIR}/reposconf" \
+
		    -o PKG_CACHEDIR="${TMPDIR}" rwhich /nonexistent/file
+
}
+

+
rwhich_multiple_packages_body() {
+
	mkdir -p usr/local/bin usr/local/lib usr/local/share
+
	touch usr/local/share/common usr/local/bin/tool1
+
	touch usr/local/bin/tool2 usr/local/lib/libfoo.so
+

+
	atf_check -s exit:0 sh ${RESOURCEDIR}/test_subr.sh new_pkg test1 test1 1 "${TMPDIR}"
+
	cat << EOF >> test1.ucl
+
files: {
+
    ${TMPDIR}/usr/local/share/common: "",
+
    ${TMPDIR}/usr/local/bin/tool1: "",
+
}
+
EOF
+
	atf_check -s exit:0 sh ${RESOURCEDIR}/test_subr.sh new_pkg test2 test2 2 "${TMPDIR}"
+
	cat << EOF >> test2.ucl
+
files: {
+
    ${TMPDIR}/usr/local/bin/tool2: "",
+
    ${TMPDIR}/usr/local/lib/libfoo.so: "",
+
}
+
EOF
+

+
	atf_check -s exit:0 pkg create -M test1.ucl
+
	atf_check -s exit:0 pkg create -M test2.ucl
+
	atf_check -o ignore -e empty -s exit:0 pkg repo -l .
+

+
	mkdir reposconf
+
	cat << EOF > reposconf/repo.conf
+
local: {
+
	url: file:///${TMPDIR},
+
	enabled: true
+
}
+
EOF
+

+
	atf_check -o ignore -e empty -s exit:0 \
+
		pkg -o REPOS_DIR="${TMPDIR}/reposconf" \
+
		    -o PKG_CACHEDIR="${TMPDIR}" update
+

+
	atf_check \
+
		-o match:"is provided by package test1-1" \
+
		-s exit:0 \
+
		pkg -o REPOS_DIR="${TMPDIR}/reposconf" \
+
		    -o PKG_CACHEDIR="${TMPDIR}" rwhich ${TMPDIR}/usr/local/bin/tool1
+

+
	atf_check \
+
		-o match:"is provided by package test2-2" \
+
		-s exit:0 \
+
		pkg -o REPOS_DIR="${TMPDIR}/reposconf" \
+
		    -o PKG_CACHEDIR="${TMPDIR}" rwhich ${TMPDIR}/usr/local/lib/libfoo.so
+
}
+

+
rwhich_no_args_body() {
+
	atf_check \
+
		-e inline:"Usage: pkg rwhich [-gq] [-r reponame] <file>\n\nFor more information see 'pkg help rwhich'.\n" \
+
		-s exit:1 \
+
		pkg rwhich
+
}
+

+
rwhich_multiple_args_body() {
+
	mkdir -p usr/local/bin usr/local/lib
+
	touch usr/local/bin/mybin usr/local/lib/libtest.so.1
+

+
	atf_check -s exit:0 sh ${RESOURCEDIR}/test_subr.sh new_pkg test test 1 "${TMPDIR}"
+
	cat << EOF >> test.ucl
+
files: {
+
    ${TMPDIR}/usr/local/bin/mybin: "",
+
    ${TMPDIR}/usr/local/lib/libtest.so.1: "",
+
}
+
EOF
+

+
	atf_check -s exit:0 pkg create -M test.ucl
+
	atf_check -o ignore -e empty -s exit:0 pkg repo -l .
+

+
	mkdir reposconf
+
	cat << EOF > reposconf/repo.conf
+
local: {
+
	url: file:///${TMPDIR},
+
	enabled: true
+
}
+
EOF
+

+
	atf_check -o ignore -e empty -s exit:0 \
+
		pkg -o REPOS_DIR="${TMPDIR}/reposconf" \
+
		    -o PKG_CACHEDIR="${TMPDIR}" update
+

+
	# Query two files at once
+
	atf_check \
+
		-o match:"usr/local/bin/mybin is provided by package test-1" \
+
		-o match:"usr/local/lib/libtest.so.1 is provided by package test-1" \
+
		-s exit:0 \
+
		pkg -o REPOS_DIR="${TMPDIR}/reposconf" \
+
		    -o PKG_CACHEDIR="${TMPDIR}" rwhich \
+
		    ${TMPDIR}/usr/local/bin/mybin ${TMPDIR}/usr/local/lib/libtest.so.1
+
}
+

+
rwhich_repo_flag_body() {
+
	mkdir -p usr/local/bin usr/local/sbin
+
	touch usr/local/bin/mybin usr/local/sbin/othertool
+

+
	atf_check -s exit:0 sh ${RESOURCEDIR}/test_subr.sh new_pkg alpha alpha 1 "${TMPDIR}"
+
	cat << EOF >> alpha.ucl
+
files: {
+
    ${TMPDIR}/usr/local/bin/mybin: "",
+
}
+
EOF
+

+
	atf_check -s exit:0 sh ${RESOURCEDIR}/test_subr.sh new_pkg beta beta 2 "${TMPDIR}"
+
	cat << EOF >> beta.ucl
+
files: {
+
    ${TMPDIR}/usr/local/sbin/othertool: "",
+
}
+
EOF
+

+
	atf_check -s exit:0 pkg create -o ${TMPDIR}/repoA -M alpha.ucl
+
	atf_check -s exit:0 pkg create -o ${TMPDIR}/repoB -M beta.ucl
+

+
	atf_check -o ignore pkg repo -l ${TMPDIR}/repoA
+
	atf_check -o ignore pkg repo -l ${TMPDIR}/repoB
+

+
	mkdir -p reposconf
+
	cat << EOF > reposconf/multi.conf
+
repoA: {
+
    url: file://${TMPDIR}/repoA,
+
    enabled: true
+
}
+
repoB: {
+
    url: file://${TMPDIR}/repoB,
+
    enabled: true
+
}
+
EOF
+

+
	atf_check -o ignore -e empty -s exit:0 \
+
		pkg -o REPOS_DIR="${TMPDIR}/reposconf" \
+
		    -o PKG_CACHEDIR="${TMPDIR}" update
+

+
	# File is in repoA, should be found when restricting to repoA
+
	atf_check \
+
		-o match:"is provided by package alpha-1" \
+
		-s exit:0 \
+
		pkg -o REPOS_DIR="${TMPDIR}/reposconf" \
+
		    -o PKG_CACHEDIR="${TMPDIR}" rwhich -r repoA \
+
		    ${TMPDIR}/usr/local/bin/mybin
+

+
	# File is not in repoB, should not be found
+
	atf_check \
+
		-o match:"was not found in the repository" \
+
		-s exit:1 \
+
		pkg -o REPOS_DIR="${TMPDIR}/reposconf" \
+
		    -o PKG_CACHEDIR="${TMPDIR}" rwhich -r repoB \
+
		    ${TMPDIR}/usr/local/bin/mybin
+
}
+

+
rwhich_shared_dirs_body() {
+
	# Verify that packages sharing the same directory are
+
	# both found via glob
+
	mkdir -p usr/local/bin
+
	touch usr/local/bin/tool1 usr/local/bin/tool2
+

+
	atf_check -s exit:0 sh ${RESOURCEDIR}/test_subr.sh new_pkg pkg1 pkg1 1 "${TMPDIR}"
+
	cat << EOF >> pkg1.ucl
+
files: {
+
    ${TMPDIR}/usr/local/bin/tool1: "",
+
}
+
EOF
+
	atf_check -s exit:0 sh ${RESOURCEDIR}/test_subr.sh new_pkg pkg2 pkg2 1 "${TMPDIR}"
+
	cat << EOF >> pkg2.ucl
+
files: {
+
    ${TMPDIR}/usr/local/bin/tool2: "",
+
}
+
EOF
+

+
	atf_check -s exit:0 pkg create -M pkg1.ucl
+
	atf_check -s exit:0 pkg create -M pkg2.ucl
+
	atf_check -o ignore -e empty -s exit:0 pkg repo -l .
+

+
	mkdir reposconf
+
	cat << EOF > reposconf/repo.conf
+
local: {
+
	url: file:///${TMPDIR},
+
	enabled: true
+
}
+
EOF
+

+
	atf_check -o ignore -e empty -s exit:0 \
+
		pkg -o REPOS_DIR="${TMPDIR}/reposconf" \
+
		    -o PKG_CACHEDIR="${TMPDIR}" update
+

+
	# Glob matching both packages in the same directory
+
	atf_check \
+
		-o match:"pkg1-1" \
+
		-o match:"pkg2-1" \
+
		-s exit:0 \
+
		pkg -o REPOS_DIR="${TMPDIR}/reposconf" \
+
		    -o PKG_CACHEDIR="${TMPDIR}" rwhich -gq "*/usr/local/bin/tool*"
+
}
+

+
rwhich_no_filelist_body() {
+
	# Repo created without -l should not have file data
+
	mkdir -p usr/local/bin
+
	touch usr/local/bin/mybin
+

+
	atf_check -s exit:0 sh ${RESOURCEDIR}/test_subr.sh new_pkg test test 1 "${TMPDIR}"
+
	cat << EOF >> test.ucl
+
files: {
+
    ${TMPDIR}/usr/local/bin/mybin: "",
+
}
+
EOF
+

+
	atf_check -s exit:0 pkg create -M test.ucl
+
	atf_check -o ignore -e empty -s exit:0 pkg repo .
+

+
	mkdir reposconf
+
	cat << EOF > reposconf/repo.conf
+
local: {
+
	url: file:///${TMPDIR},
+
	enabled: true
+
}
+
EOF
+

+
	atf_check -o ignore -e empty -s exit:0 \
+
		pkg -o REPOS_DIR="${TMPDIR}/reposconf" \
+
		    -o PKG_CACHEDIR="${TMPDIR}" update
+

+
	# Without filelist data, rwhich should not find anything
+
	atf_check \
+
		-o match:"was not found in the repository" \
+
		-s exit:1 \
+
		pkg -o REPOS_DIR="${TMPDIR}/reposconf" \
+
		    -o PKG_CACHEDIR="${TMPDIR}" rwhich ${TMPDIR}/usr/local/bin/mybin
+
}