1/*	$NetBSD: iterate.c,v 1.4 2021/04/10 19:49:59 nia Exp $	*/
2
3/*-
4 * Copyright (c) 2007 Joerg Sonnenberger <joerg@NetBSD.org>.
5 * All rights reserved.
6 *
7 * Redistribution and use in source and binary forms, with or without
8 * modification, are permitted provided that the following conditions
9 * are met:
10 *
11 * 1. Redistributions of source code must retain the above copyright
12 *    notice, this list of conditions and the following disclaimer.
13 * 2. Redistributions in binary form must reproduce the above copyright
14 *    notice, this list of conditions and the following disclaimer in
15 *    the documentation and/or other materials provided with the
16 *    distribution.
17 *
18 * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS
19 * ``AS IS'' AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT
20 * LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS
21 * FOR A PARTICULAR PURPOSE ARE DISCLAIMED.  IN NO EVENT SHALL THE
22 * COPYRIGHT HOLDERS OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT,
23 * INCIDENTAL, SPECIAL, EXEMPLARY OR CONSEQUENTIAL DAMAGES (INCLUDING,
24 * BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES;
25 * LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED
26 * AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY,
27 * OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT
28 * OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF
29 * SUCH DAMAGE.
30 */
31
32#if HAVE_CONFIG_H
33#include "config.h"
34#endif
35
36#include <nbcompat.h>
37
38#if HAVE_ERR_H
39#include <err.h>
40#endif
41#if HAVE_ERRNO_H
42#include <errno.h>
43#endif
44
45#include "lib.h"
46
47/*
48 * We define a couple of different caches to hold frequently accessed data.
49 *
50 * Firstly, we cache the results of readdir() on the package database directory
51 * when using iterate_pkg_db_cached().  This helps a lot during recursive calls
52 * and avoids exponential system calls, but is not suitable for situations
53 * where the database directory may be updated, for example during installs.
54 * In those situations the regular iterate_pkg_db() must be used.
55 *
56 * Secondly, we have a cache for matches of pattern lookups, avoiding expensive
57 * pkg_match() calls each time.
58 */
59struct pkg_db_list {
60	char *pkgname;
61	SLIST_ENTRY(pkg_db_list) entries;
62};
63SLIST_HEAD(pkg_db_list_head, pkg_db_list);
64
65struct pkg_match_list {
66	char *pattern;
67	char *pkgname;
68	SLIST_ENTRY(pkg_match_list) entries;
69};
70SLIST_HEAD(pkg_match_list_head, pkg_match_list);
71
72static struct pkg_db_list_head pkg_list_cache;
73static struct pkg_match_list_head pkg_match_cache[PKG_HASH_SIZE];
74
75/*
76 * Generic iteration function:
77 * - get new entries from srciter, stop on NULL
78 * - call matchiter for those entries, stop on non-null return value.
79 */
80int
81iterate_pkg_generic_src(int (*matchiter)(const char *, void *),
82    void *match_cookie, const char *(*srciter)(void *), void *src_cookie)
83{
84	int retval;
85	const char *entry;
86
87	retval = 0;
88
89	while ((entry = (*srciter)(src_cookie)) != NULL) {
90		if ((retval = (*matchiter)(entry, match_cookie)) != 0)
91			break;
92	}
93
94	return retval;
95}
96
97struct pkg_dir_iter_arg {
98	DIR *dirp;
99	int filter_suffix;
100	int allow_nonfiles;
101};
102
103static const char *
104pkg_dir_iter(void *cookie)
105{
106	struct pkg_dir_iter_arg *arg = cookie;
107	struct dirent *dp;
108	size_t len;
109
110	while ((dp = readdir(arg->dirp)) != NULL) {
111#if defined(DT_UNKNOWN) && defined(DT_DIR)
112		if (arg->allow_nonfiles == 0 &&
113		    dp->d_type != DT_UNKNOWN && dp->d_type != DT_REG)
114			continue;
115#endif
116		len = strlen(dp->d_name);
117		/* .tbz or .tgz suffix length + some prefix*/
118		if (len < 5)
119			continue;
120		if (arg->filter_suffix == 0 ||
121		    memcmp(dp->d_name + len - 4, ".tgz", 4) == 0 ||
122		    memcmp(dp->d_name + len - 4, ".tbz", 4) == 0)
123			return dp->d_name;
124	}
125	return NULL;
126}
127
128/*
129 * Call matchiter for every package in the directory.
130 */
131int
132iterate_local_pkg_dir(const char *dir, int filter_suffix, int allow_nonfiles,
133    int (*matchiter)(const char *, void *), void *cookie)
134{
135	struct pkg_dir_iter_arg arg;
136	int retval;
137
138	if ((arg.dirp = opendir(dir)) == NULL)
139		return -1;
140
141	arg.filter_suffix = filter_suffix;
142	arg.allow_nonfiles = allow_nonfiles;
143	retval = iterate_pkg_generic_src(matchiter, cookie, pkg_dir_iter, &arg);
144
145	if (closedir(arg.dirp) == -1)
146		return -1;
147	return retval;
148}
149
150static const char *
151pkg_db_iter(void *cookie)
152{
153	DIR *dirp = cookie;
154	struct dirent *dp;
155
156	while ((dp = readdir(dirp)) != NULL) {
157		if (strcmp(dp->d_name, ".") == 0)
158			continue;
159		if (strcmp(dp->d_name, "..") == 0)
160			continue;
161		if (strcmp(dp->d_name, "pkgdb.byfile.db") == 0)
162			continue;
163		if (strcmp(dp->d_name, ".cookie") == 0)
164			continue;
165		if (strcmp(dp->d_name, "pkg-vulnerabilities") == 0)
166			continue;
167#if defined(DT_UNKNOWN) && defined(DT_DIR)
168		if (dp->d_type != DT_UNKNOWN && dp->d_type != DT_DIR)
169			continue;
170#endif
171		return dp->d_name;
172	}
173	return NULL;
174}
175
176/*
177 * Call matchiter for every installed package.
178 */
179int
180iterate_pkg_db(int (*matchiter)(const char *, void *), void *cookie)
181{
182	DIR *dirp;
183	int retval;
184
185	if ((dirp = opendir(pkgdb_get_dir())) == NULL) {
186		if (errno == ENOENT)
187			return 0; /* No pkgdb directory == empty pkgdb */
188		return -1;
189	}
190
191	retval = iterate_pkg_generic_src(matchiter, cookie, pkg_db_iter, dirp);
192
193	if (closedir(dirp) == -1)
194		return -1;
195	return retval;
196}
197
198struct pkg_db_iter_arg {
199	struct pkg_db_list_head head;
200	struct pkg_db_list *list;
201};
202
203static const char *
204pkg_db_iter_cached(void *cookie)
205{
206	struct pkg_db_iter_arg *arg = cookie;
207
208	if (arg->list == NULL)
209		arg->list = SLIST_FIRST(&arg->head);
210	else
211		arg->list = SLIST_NEXT(arg->list, entries);
212
213	if (arg->list != NULL)
214		return arg->list->pkgname;
215
216	return NULL;
217}
218
219/*
220 * Call matchiter for every installed package, using cached data to
221 * significantly increase performance during recursive calls.
222 *
223 * This is not suitable for every situation, for example when finding new
224 * matches after package installation/removal.  In those situations the
225 * regular iterate_pkg_db() must be used.
226 */
227static int
228iterate_pkg_db_cached(int (*matchiter)(const char *, void *), void *cookie)
229{
230	DIR *dirp;
231	struct pkg_db_iter_arg arg;
232	struct pkg_db_list *pkg;
233	const char *pkgdir;
234	int retval;
235
236	if (SLIST_EMPTY(&pkg_list_cache)) {
237		SLIST_INIT(&pkg_list_cache);
238
239		if ((dirp = opendir(pkgdb_get_dir())) == NULL) {
240			if (errno == ENOENT)
241				return 0; /* Empty pkgdb */
242			return -1;
243		}
244
245		while ((pkgdir = pkg_db_iter(dirp)) != NULL) {
246			pkg = xmalloc(sizeof(struct pkg_db_list));
247			pkg->pkgname = xstrdup(pkgdir);
248			SLIST_INSERT_HEAD(&pkg_list_cache, pkg, entries);
249		}
250
251		if (closedir(dirp) == -1)
252			return -1;
253	}
254
255	arg.head = pkg_list_cache;
256	arg.list = NULL;
257
258	retval = iterate_pkg_generic_src(matchiter, cookie,
259	    pkg_db_iter_cached, &arg);
260
261	return retval;
262}
263
264static int
265match_by_basename(const char *pkg, void *cookie)
266{
267	const char *target = cookie;
268	const char *pkg_version;
269
270	if ((pkg_version = strrchr(pkg, '-')) == NULL) {
271		warnx("Entry %s in pkgdb is not a valid package name", pkg);
272		return 0;
273	}
274	if (strncmp(pkg, target, pkg_version - pkg) == 0 &&
275	    pkg + strlen(target) == pkg_version)
276		return 1;
277	else
278		return 0;
279}
280
281static int
282match_by_pattern(const char *pkg, void *cookie)
283{
284	const char *pattern = cookie;
285
286	return pkg_match(pattern, pkg);
287}
288
289struct add_matching_arg {
290	lpkg_head_t *pkghead;
291	int got_match;
292	int (*match_fn)(const char *pkg, void *cookie);
293	void *cookie;
294};
295
296static int
297match_and_add(const char *pkg, void *cookie)
298{
299	struct add_matching_arg *arg = cookie;
300	lpkg_t *lpp;
301
302	if ((*arg->match_fn)(pkg, arg->cookie) == 1) {
303		arg->got_match = 1;
304
305		lpp = alloc_lpkg(pkg);
306		TAILQ_INSERT_TAIL(arg->pkghead, lpp, lp_link);
307	}
308	return 0;
309}
310
311/*
312 * Find all installed packages with the given basename and add them
313 * to pkghead.
314 * Returns -1 on error, 0 if no match was found and 1 otherwise.
315 */
316int
317add_installed_pkgs_by_basename(const char *pkgbase, lpkg_head_t *pkghead)
318{
319	struct add_matching_arg arg;
320
321	arg.pkghead = pkghead;
322	arg.got_match = 0;
323	arg.match_fn = match_by_basename;
324	arg.cookie = __UNCONST(pkgbase);
325
326	if (iterate_pkg_db(match_and_add, &arg) == -1) {
327		warnx("could not process pkgdb");
328		return -1;
329	}
330	return arg.got_match;
331}
332
333/*
334 * Match all installed packages against pattern, add the matches to pkghead.
335 * Returns -1 on error, 0 if no match was found and 1 otherwise.
336 */
337int
338add_installed_pkgs_by_pattern(const char *pattern, lpkg_head_t *pkghead)
339{
340	struct add_matching_arg arg;
341
342	arg.pkghead = pkghead;
343	arg.got_match = 0;
344	arg.match_fn = match_by_pattern;
345	arg.cookie = __UNCONST(pattern);
346
347	if (iterate_pkg_db(match_and_add, &arg) == -1) {
348		warnx("could not process pkgdb");
349		return -1;
350	}
351	return arg.got_match;
352}
353
354struct best_installed_match_arg {
355	const char *pattern;
356	char *best_current_match;
357};
358
359static int
360match_best_installed(const char *pkg, void *cookie)
361{
362	struct best_installed_match_arg *arg = cookie;
363
364	switch (pkg_order(arg->pattern, pkg, arg->best_current_match)) {
365	case 0:
366	case 2:
367		/*
368		 * Either current package doesn't match or
369		 * the older match is better. Nothing to do.
370		 */
371		break;
372	case 1:
373		/* Current package is better, remember it. */
374		free(arg->best_current_match);
375		arg->best_current_match = xstrdup(pkg);
376		break;
377	}
378	return 0;
379}
380
381/*
382 * Returns a copy of the name of best matching package.
383 * If no package matched the pattern or an error occured, return NULL.
384 *
385 * If use_cached is set, return a cached match entry if it exists, and also use
386 * the iterate_pkg_db cache, otherwise clear any matching cache entry and use
387 * regular iterate_pkg_db().
388 */
389char *
390find_best_matching_installed_pkg(const char *pattern, int use_cached)
391{
392	struct best_installed_match_arg arg;
393	struct pkg_match_list *pkg;
394	int idx = PKG_HASH_ENTRY(pattern), rv;
395
396	if (pattern == NULL)
397		return NULL;
398
399	SLIST_FOREACH(pkg, &pkg_match_cache[idx], entries) {
400		if (strcmp(pattern, pkg->pattern) == 0) {
401			if (use_cached)
402				return xstrdup(pkg->pkgname);
403			SLIST_REMOVE(&pkg_match_cache[idx], pkg,
404			    pkg_match_list, entries);
405			free(pkg->pattern);
406			free(pkg->pkgname);
407			free(pkg);
408			break;
409		}
410	}
411
412	arg.pattern = pattern;
413	arg.best_current_match = NULL;
414
415	if (use_cached)
416		rv = iterate_pkg_db_cached(match_best_installed, &arg);
417	else
418		rv = iterate_pkg_db(match_best_installed, &arg);
419
420	if (rv == -1) {
421		warnx("could not process pkgdb");
422		return NULL;
423	}
424
425	if (arg.best_current_match != NULL) {
426		pkg = xmalloc(sizeof(struct pkg_match_list));
427		pkg->pattern = xstrdup(pattern);
428		pkg->pkgname = xstrdup(arg.best_current_match);
429		SLIST_INSERT_HEAD(&pkg_match_cache[idx],
430		    pkg, entries);
431	}
432
433	return arg.best_current_match;
434}
435
436struct call_matching_arg {
437	const char *pattern;
438	int (*call_fn)(const char *pkg, void *cookie);
439	void *cookie;
440};
441
442static int
443match_and_call(const char *pkg, void *cookie)
444{
445	struct call_matching_arg *arg = cookie;
446
447	if (pkg_match(arg->pattern, pkg) == 1) {
448		return (*arg->call_fn)(pkg, arg->cookie);
449	} else
450		return 0;
451}
452
453/*
454 * Find all packages that match the given pattern and call the function
455 * for each of them. Iteration stops if the callback return non-0.
456 * Returns -1 on error, 0 if the iteration finished or whatever the
457 * callback returned otherwise.
458 */
459int
460match_installed_pkgs(const char *pattern, int (*cb)(const char *, void *),
461    void *cookie)
462{
463	struct call_matching_arg arg;
464
465	arg.pattern = pattern;
466	arg.call_fn = cb;
467	arg.cookie = cookie;
468
469	return iterate_pkg_db(match_and_call, &arg);
470}
471
472struct best_file_match_arg {
473	const char *pattern;
474	char *best_current_match_filtered;
475	char *best_current_match;
476	int filter_suffix;
477};
478
479static int
480match_best_file(const char *filename, void *cookie)
481{
482	struct best_file_match_arg *arg = cookie;
483	const char *active_filename;
484	char *filtered_filename;
485
486	if (arg->filter_suffix) {
487		size_t len;
488
489		len = strlen(filename);
490		if (len < 5 ||
491		    (memcmp(filename + len - 4, ".tgz", 4) != 0 &&
492		     memcmp(filename + len - 4, ".tbz", 4) != 0)) {
493			warnx("filename %s does not contain a recognized suffix", filename);
494			return -1;
495		}
496		filtered_filename = xmalloc(len - 4 + 1);
497		memcpy(filtered_filename, filename, len - 4);
498		filtered_filename[len - 4] = '\0';
499		active_filename = filtered_filename;
500	} else {
501		filtered_filename = NULL;
502		active_filename = filename;
503	}
504
505	switch (pkg_order(arg->pattern, active_filename, arg->best_current_match_filtered)) {
506	case 0:
507	case 2:
508		/*
509		 * Either current package doesn't match or
510		 * the older match is better. Nothing to do.
511		 */
512		free(filtered_filename);
513		return 0;
514	case 1:
515		/* Current package is better, remember it. */
516		free(arg->best_current_match);
517		free(arg->best_current_match_filtered);
518		arg->best_current_match = xstrdup(filename);
519		if (filtered_filename != NULL)
520			arg->best_current_match_filtered = filtered_filename;
521		else
522			arg->best_current_match_filtered = xstrdup(active_filename);
523		return 0;
524	default:
525		errx(EXIT_FAILURE, "Invalid error from pkg_order");
526		/* NOTREACHED */
527	}
528}
529
530/*
531 * Returns a copy of the name of best matching file.
532 * If no package matched the pattern or an error occured, return NULL.
533 */
534char *
535find_best_matching_file(const char *dir, const char *pattern, int filter_suffix, int allow_nonfiles)
536{
537	struct best_file_match_arg arg;
538
539	arg.filter_suffix = filter_suffix;
540	arg.pattern = pattern;
541	arg.best_current_match = NULL;
542	arg.best_current_match_filtered = NULL;
543
544	if (iterate_local_pkg_dir(dir, filter_suffix, allow_nonfiles, match_best_file, &arg) == -1) {
545		warnx("could not process directory");
546		return NULL;
547	}
548	free(arg.best_current_match_filtered);
549
550	return arg.best_current_match;
551}
552
553struct call_matching_file_arg {
554	const char *pattern;
555	int (*call_fn)(const char *pkg, void *cookie);
556	void *cookie;
557	int filter_suffix;
558};
559
560static int
561match_file_and_call(const char *filename, void *cookie)
562{
563	struct call_matching_file_arg *arg = cookie;
564	const char *active_filename;
565	char *filtered_filename;
566	int ret;
567
568	if (arg->filter_suffix) {
569		size_t len;
570
571		len = strlen(filename);
572		if (len < 5 ||
573		    (memcmp(filename + len - 4, ".tgz", 4) != 0 &&
574		     memcmp(filename + len - 4, ".tbz", 4) != 0)) {
575			warnx("filename %s does not contain a recognized suffix", filename);
576			return -1;
577		}
578		filtered_filename = xmalloc(len - 4 + 1);
579		memcpy(filtered_filename, filename, len - 4);
580		filtered_filename[len - 4] = '\0';
581		active_filename = filtered_filename;
582	} else {
583		filtered_filename = NULL;
584		active_filename = filename;
585	}
586
587	ret = pkg_match(arg->pattern, active_filename);
588	free(filtered_filename);
589
590	if (ret == 1)
591		return (*arg->call_fn)(filename, arg->cookie);
592	else
593		return 0;
594}
595
596/*
597 * Find all packages that match the given pattern and call the function
598 * for each of them. Iteration stops if the callback return non-0.
599 * Returns -1 on error, 0 if the iteration finished or whatever the
600 * callback returned otherwise.
601 */
602int
603match_local_files(const char *dir, int filter_suffix, int allow_nonfiles, const char *pattern,
604    int (*cb)(const char *, void *), void *cookie)
605{
606	struct call_matching_file_arg arg;
607
608	arg.pattern = pattern;
609	arg.call_fn = cb;
610	arg.cookie = cookie;
611	arg.filter_suffix = filter_suffix;
612
613	return iterate_local_pkg_dir(dir, filter_suffix, allow_nonfiles, match_file_and_call, &arg);
614}
615