match.c revision 178103
1/*
2 * FreeBSD install - a package for the installation and maintainance
3 * of non-core utilities.
4 *
5 * Redistribution and use in source and binary forms, with or without
6 * modification, are permitted provided that the following conditions
7 * are met:
8 * 1. Redistributions of source code must retain the above copyright
9 *    notice, this list of conditions and the following disclaimer.
10 * 2. Redistributions in binary form must reproduce the above copyright
11 *    notice, this list of conditions and the following disclaimer in the
12 *    documentation and/or other materials provided with the distribution.
13 *
14 * Maxim Sobolev
15 * 24 February 2001
16 *
17 * Routines used to query installed packages.
18 *
19 */
20
21#include <sys/cdefs.h>
22__FBSDID("$FreeBSD: head/usr.sbin/pkg_install/lib/match.c 178103 2008-04-11 08:26:06Z pav $");
23
24#include "lib.h"
25#include <err.h>
26#include <fnmatch.h>
27#include <fts.h>
28#include <regex.h>
29
30/*
31 * Simple structure representing argv-like
32 * NULL-terminated list.
33 */
34struct store {
35    int currlen;
36    int used;
37    char **store;
38};
39
40static int rex_match(const char *, const char *, int);
41static int csh_match(const char *, const char *, int);
42struct store *storecreate(struct store *);
43static int storeappend(struct store *, const char *);
44static int fname_cmp(const FTSENT * const *, const FTSENT * const *);
45
46/*
47 * Function to query names of installed packages.
48 * MatchType	- one of MATCH_ALL, MATCH_EREGEX, MATCH_REGEX, MATCH_GLOB, MATCH_NGLOB;
49 * patterns	- NULL-terminated list of glob or regex patterns
50 *		  (could be NULL for MATCH_ALL);
51 * retval	- return value (could be NULL if you don't want/need
52 *		  return value).
53 * Returns NULL-terminated list with matching names.
54 * Names in list returned are dynamically allocated and should
55 * not be altered by the caller.
56 */
57char **
58matchinstalled(match_t MatchType, char **patterns, int *retval)
59{
60    int i, errcode, len;
61    char *matched;
62    const char *paths[2] = {LOG_DIR, NULL};
63    static struct store *store = NULL;
64    FTS *ftsp;
65    FTSENT *f;
66    Boolean *lmatched = NULL;
67
68    store = storecreate(store);
69    if (store == NULL) {
70	if (retval != NULL)
71	    *retval = 1;
72	return NULL;
73    }
74
75    if (retval != NULL)
76	*retval = 0;
77
78    if (!isdir(paths[0])) {
79	if (retval != NULL)
80	    *retval = 1;
81	return NULL;
82	/* Not reached */
83    }
84
85    /* Count number of patterns */
86    if (patterns != NULL) {
87	for (len = 0; patterns[len]; len++) {}
88	lmatched = alloca(sizeof(*lmatched) * len);
89	if (lmatched == NULL) {
90	    warnx("%s(): alloca() failed", __func__);
91	    if (retval != NULL)
92		*retval = 1;
93	    return NULL;
94    	}
95    } else
96	len = 0;
97
98    for (i = 0; i < len; i++)
99	lmatched[i] = FALSE;
100
101    ftsp = fts_open((char * const *)(uintptr_t)paths, FTS_LOGICAL | FTS_NOCHDIR | FTS_NOSTAT, fname_cmp);
102    if (ftsp != NULL) {
103	while ((f = fts_read(ftsp)) != NULL) {
104	    if (f->fts_info == FTS_D && f->fts_level == 1) {
105		fts_set(ftsp, f, FTS_SKIP);
106		matched = NULL;
107		errcode = 0;
108		if (MatchType == MATCH_ALL)
109		    matched = f->fts_name;
110		else
111		    for (i = 0; patterns[i]; i++) {
112			errcode = pattern_match(MatchType, patterns[i], f->fts_name);
113			if (errcode == 1) {
114			    matched = f->fts_name;
115			    lmatched[i] = TRUE;
116			    errcode = 0;
117			}
118			if (matched != NULL || errcode != 0)
119			    break;
120		    }
121		if (errcode == 0 && matched != NULL)
122		    errcode = storeappend(store, matched);
123		if (errcode != 0) {
124		    if (retval != NULL)
125			*retval = 1;
126		    return NULL;
127		    /* Not reached */
128		}
129	    }
130	}
131	fts_close(ftsp);
132    }
133
134    if (MatchType == MATCH_GLOB) {
135	for (i = 0; i < len; i++)
136	    if (lmatched[i] == FALSE)
137		storeappend(store, patterns[i]);
138    }
139
140    if (store->used == 0)
141	return NULL;
142    else
143	return store->store;
144}
145
146int
147pattern_match(match_t MatchType, char *pattern, const char *pkgname)
148{
149    int errcode = 0;
150    const char *fname = pkgname;
151    char basefname[PATH_MAX];
152    char condchar = '\0';
153    char *condition;
154
155    /* do we have an appended condition? */
156    condition = strpbrk(pattern, "<>=");
157    if (condition) {
158	const char *ch;
159	/* yes, isolate the pattern from the condition ... */
160	if (condition > pattern && condition[-1] == '!')
161	    condition--;
162	condchar = *condition;
163	*condition = '\0';
164	/* ... and compare the name without version */
165	ch = strrchr(fname, '-');
166	if (ch && ch - fname < PATH_MAX) {
167	    strlcpy(basefname, fname, ch - fname + 1);
168	    fname = basefname;
169	}
170    }
171
172    switch (MatchType) {
173    case MATCH_EREGEX:
174    case MATCH_REGEX:
175	errcode = rex_match(pattern, fname, MatchType == MATCH_EREGEX ? 1 : 0);
176	break;
177    case MATCH_NGLOB:
178    case MATCH_GLOB:
179	errcode = (csh_match(pattern, fname, 0) == 0) ? 1 : 0;
180	break;
181    case MATCH_EXACT:
182	errcode = (strcmp(pattern, fname) == 0) ? 1 : 0;
183	break;
184    case MATCH_ALL:
185	errcode = 1;
186	break;
187    default:
188	break;
189    }
190
191    /* loop over all appended conditions */
192    while (condition) {
193	/* restore the pattern */
194	*condition = condchar;
195	/* parse the condition (fun with bits) */
196	if (errcode == 1) {
197	    char *nextcondition;
198	    /* compare version numbers */
199	    int match = 0;
200	    if (*++condition == '=') {
201		match = 2;
202		condition++;
203	    }
204	    switch(condchar) {
205	    case '<':
206		match |= 1;
207		break;
208	    case '>':
209		match |= 4;
210		break;
211	    case '=':
212		match |= 2;
213		break;
214	    case '!':
215		match = 5;
216		break;
217	    }
218	    /* isolate the version number from the next condition ... */
219	    nextcondition = strpbrk(condition, "<>=!");
220	    if (nextcondition) {
221		condchar = *nextcondition;
222		*nextcondition = '\0';
223	    }
224	    /* and compare the versions (version_cmp removes the filename for us) */
225	    if ((match & (1 << (version_cmp(pkgname, condition) + 1))) == 0)
226		errcode = 0;
227	    condition = nextcondition;
228	} else {
229	    break;
230	}
231    }
232
233    return errcode;
234}
235
236/*
237 * Synopsis is similar to matchinstalled(), but use origin
238 * as a key for matching packages.
239 */
240char **
241matchallbyorigin(const char **origins, int *retval)
242{
243    char **installed, **allorigins = NULL, **matches = NULL;
244    int i, j;
245
246    if (retval != NULL)
247	*retval = 0;
248
249    installed = matchinstalled(MATCH_ALL, NULL, retval);
250    if (installed == NULL)
251	return NULL;
252
253    /* Gather origins for all installed packages */
254    for (i = 0; installed[i] != NULL; i++) {
255	FILE *fp;
256	char *buf, *cp, tmp[PATH_MAX];
257	int cmd;
258
259	allorigins = realloc(allorigins, (i + 1) * sizeof(*allorigins));
260	allorigins[i] = NULL;
261
262	snprintf(tmp, PATH_MAX, "%s/%s", LOG_DIR, installed[i]);
263	/*
264	 * SPECIAL CASE: ignore empty dirs, since we can can see them
265	 * during port installation.
266	 */
267	if (isemptydir(tmp))
268	    continue;
269	snprintf(tmp, PATH_MAX, "%s/%s", tmp, CONTENTS_FNAME);
270	fp = fopen(tmp, "r");
271	if (fp == NULL) {
272	    warnx("the package info for package '%s' is corrupt", installed[i]);
273	    continue;
274	}
275
276	cmd = -1;
277	while (fgets(tmp, sizeof(tmp), fp)) {
278	    int len = strlen(tmp);
279
280	    while (len && isspace(tmp[len - 1]))
281		tmp[--len] = '\0';
282	    if (!len)
283		continue;
284	    cp = tmp;
285	    if (tmp[0] != CMD_CHAR)
286		continue;
287	    cmd = plist_cmd(tmp + 1, &cp);
288	    if (cmd == PLIST_ORIGIN) {
289		asprintf(&buf, "%s", cp);
290		allorigins[i] = buf;
291		break;
292	    }
293	}
294	if (cmd != PLIST_ORIGIN && ( Verbose || 0 != strncmp("bsdpan-", installed[i], 7 ) ) )
295	    warnx("package %s has no origin recorded", installed[i]);
296	fclose(fp);
297    }
298
299    /* Resolve origins into package names, retaining the sequence */
300    for (i = 0; origins[i] != NULL; i++) {
301	matches = realloc(matches, (i + 1) * sizeof(*matches));
302	matches[i] = NULL;
303
304	for (j = 0; installed[j] != NULL; j++) {
305	    if (allorigins[j]) {
306		if (csh_match(origins[i], allorigins[j], FNM_PATHNAME) == 0) {
307		    matches[i] = installed[j];
308		    break;
309		}
310	    }
311	}
312    }
313
314    if (allorigins) {
315	for (i = 0; installed[i] != NULL; i++)
316	    if (allorigins[i])
317		free(allorigins[i]);
318	free(allorigins);
319    }
320
321    return matches;
322}
323
324/*
325 * Synopsis is similar to matchinstalled(), but use origin
326 * as a key for matching packages.
327 */
328char **
329matchbyorigin(const char *origin, int *retval)
330{
331   const char *origins[2];
332   char **tmp;
333
334   origins[0] = origin;
335   origins[1] = NULL;
336
337   tmp = matchallbyorigin(origins, retval);
338   if (tmp && tmp[0]) {
339	tmp = realloc(tmp, 2 * sizeof(*tmp));
340	tmp[1] = NULL;
341	return tmp;
342   } else {
343	return NULL;
344   }
345}
346
347/*
348 * Small linked list to memoize results of isinstalledpkg().  A hash table
349 * would be faster but for n ~= 1000 may be overkill.
350 */
351struct iip_memo {
352	LIST_ENTRY(iip_memo) iip_link;
353	char	*iip_name;
354	int	 iip_result;
355};
356LIST_HEAD(, iip_memo) iip_memo = LIST_HEAD_INITIALIZER(iip_memo);
357
358/*
359 *
360 * Return 1 if the specified package is installed,
361 * 0 if not, and -1 if an error occured.
362 */
363int
364isinstalledpkg(const char *name)
365{
366    int result;
367    char *buf, *buf2;
368    struct iip_memo *memo;
369
370    LIST_FOREACH(memo, &iip_memo, iip_link) {
371	if (strcmp(memo->iip_name, name) == 0)
372	    return memo->iip_result;
373    }
374
375    buf2 = NULL;
376    asprintf(&buf, "%s/%s", LOG_DIR, name);
377    if (buf == NULL)
378	goto errout;
379    if (!isdir(buf) || access(buf, R_OK) == FAIL) {
380	result = 0;
381    } else {
382	asprintf(&buf2, "%s/%s", buf, CONTENTS_FNAME);
383	if (buf2 == NULL)
384	    goto errout;
385
386	if (!isfile(buf2) || access(buf2, R_OK) == FAIL)
387	    result = -1;
388	else
389	    result = 1;
390    }
391
392    free(buf);
393    buf = strdup(name);
394    if (buf == NULL)
395	goto errout;
396    free(buf2);
397    buf2 = NULL;
398
399    memo = malloc(sizeof *memo);
400    if (memo == NULL)
401	goto errout;
402    memo->iip_name = buf;
403    memo->iip_result = result;
404    LIST_INSERT_HEAD(&iip_memo, memo, iip_link);
405    return result;
406
407errout:
408    if (buf != NULL)
409	free(buf);
410    if (buf2 != NULL)
411	free(buf2);
412    return -1;
413}
414
415/*
416 * Returns 1 if specified pkgname matches RE pattern.
417 * Otherwise returns 0 if doesn't match or -1 if RE
418 * engine reported an error (usually invalid syntax).
419 */
420static int
421rex_match(const char *pattern, const char *pkgname, int extended)
422{
423    char errbuf[128];
424    int errcode;
425    int retval;
426    regex_t rex;
427
428    retval = 0;
429
430    errcode = regcomp(&rex, pattern, (extended ? REG_EXTENDED : REG_BASIC) | REG_NOSUB);
431    if (errcode == 0)
432	errcode = regexec(&rex, pkgname, 0, NULL, 0);
433
434    if (errcode == 0) {
435	retval = 1;
436    } else if (errcode != REG_NOMATCH) {
437	regerror(errcode, &rex, errbuf, sizeof(errbuf));
438	warnx("%s: %s", pattern, errbuf);
439	retval = -1;
440    }
441
442    regfree(&rex);
443
444    return retval;
445}
446
447/*
448 * Match string by a csh-style glob pattern. Returns 0 on
449 * match and FNM_NOMATCH otherwise, to be compatible with
450 * fnmatch(3).
451 */
452static int
453csh_match(const char *pattern, const char *string, int flags)
454{
455    int ret = FNM_NOMATCH;
456
457
458    const char *nextchoice = pattern;
459    const char *current = NULL;
460
461    int prefixlen = -1;
462    int currentlen = 0;
463
464    int level = 0;
465
466    do {
467	const char *pos = nextchoice;
468	const char *postfix = NULL;
469
470	Boolean quoted = FALSE;
471
472	nextchoice = NULL;
473
474	do {
475	    const char *eb;
476	    if (!*pos) {
477		postfix = pos;
478	    } else if (quoted) {
479		quoted = FALSE;
480	    } else {
481		switch (*pos) {
482		case '{':
483		    ++level;
484		    if (level == 1) {
485			current = pos+1;
486			prefixlen = pos-pattern;
487		    }
488		    break;
489		case ',':
490		    if (level == 1 && !nextchoice) {
491			nextchoice = pos+1;
492			currentlen = pos-current;
493		    }
494		    break;
495		case '}':
496		    if (level == 1) {
497			postfix = pos+1;
498			if (!nextchoice)
499			    currentlen = pos-current;
500		    }
501		    level--;
502		    break;
503		case '[':
504		    eb = pos+1;
505		    if (*eb == '!' || *eb == '^')
506			eb++;
507		    if (*eb == ']')
508			eb++;
509		    while(*eb && *eb != ']')
510			eb++;
511		    if (*eb)
512			pos=eb;
513		    break;
514		case '\\':
515		    quoted = TRUE;
516		    break;
517		default:
518		    ;
519		}
520	    }
521	    pos++;
522	} while (!postfix);
523
524	if (current) {
525	    char buf[FILENAME_MAX];
526	    snprintf(buf, sizeof(buf), "%.*s%.*s%s", prefixlen, pattern, currentlen, current, postfix);
527	    ret = csh_match(buf, string, flags);
528	    if (ret) {
529		current = nextchoice;
530		level = 1;
531	    } else
532		current = NULL;
533	} else
534	    ret = fnmatch(pattern, string, flags);
535    } while (current);
536
537    return ret;
538}
539
540/*
541 * Create an empty store, optionally deallocating
542 * any previously allocated space if store != NULL.
543 */
544struct store *
545storecreate(struct store *store)
546{
547    int i;
548
549    if (store == NULL) {
550	store = malloc(sizeof *store);
551	if (store == NULL) {
552	    warnx("%s(): malloc() failed", __func__);
553	    return NULL;
554	}
555	store->currlen = 0;
556	store->store = NULL;
557    } else if (store->store != NULL) {
558	    /* Free previously allocated memory */
559	    for (i = 0; store->store[i] != NULL; i++)
560		free(store->store[i]);
561	    store->store[0] = NULL;
562    }
563    store->used = 0;
564
565    return store;
566}
567
568/*
569 * Append specified element to the provided store.
570 */
571static int
572storeappend(struct store *store, const char *item)
573{
574    if (store->used + 2 > store->currlen) {
575	store->currlen += 16;
576	store->store = reallocf(store->store,
577				store->currlen * sizeof(*(store->store)));
578	if (store->store == NULL) {
579	    store->currlen = 0;
580	    warnx("%s(): reallocf() failed", __func__);
581	    return 1;
582	}
583    }
584
585    asprintf(&(store->store[store->used]), "%s", item);
586    if (store->store[store->used] == NULL) {
587	warnx("%s(): malloc() failed", __func__);
588	return 1;
589    }
590    store->used++;
591    store->store[store->used] = NULL;
592
593    return 0;
594}
595
596static int
597fname_cmp(const FTSENT * const *a, const FTSENT * const *b)
598{
599    return strcmp((*a)->fts_name, (*b)->fts_name);
600}
601