xbps/bin/xbps-checkvers/main.c
2020-04-24 11:44:19 +02:00

807 lines
19 KiB
C

/*-
* Licensed under the SPDX BSD-2-Clause identifier.
* Use is subject to license terms, as specified in the LICENSE file.
*/
#include <getopt.h>
#include <stdbool.h>
#include <stdio.h>
#include <stddef.h>
#include <stdlib.h>
#include <string.h>
#include <errno.h>
#include <ctype.h>
#include <dirent.h>
#include <limits.h>
#include <sys/stat.h>
#include <assert.h>
#include <xbps.h>
#define GOT_PKGNAME_VAR 0x1
#define GOT_VERSION_VAR 0x2
#define GOT_REVISION_VAR 0x4
typedef struct _rcv_t {
const char *prog, *fname, *format;
char *xbps_conf, *rootdir, *distdir, *buf, *ptr, *cachefile;
size_t bufsz, len;
uint8_t have_vars;
bool show_all, manual, installed, removed, show_removed;
xbps_dictionary_t env;
xbps_dictionary_t pkgd;
xbps_dictionary_t cache;
xbps_dictionary_t templates;
struct xbps_handle xhp;
} rcv_t;
typedef int (*rcv_check_func)(rcv_t *);
typedef int (*rcv_proc_func)(rcv_t *, const char *, rcv_check_func);
static char *
xstrdup(const char *src)
{
char *p;
if (!(p = strdup(src))) {
fprintf(stderr, "Error: %s\n", strerror(errno));
exit(1);
}
return p;
}
static int
show_usage(const char *prog, bool fail)
{
fprintf(stderr,
"Usage: %s [OPTIONS] [FILES...]\n\n"
"OPTIONS:\n"
" -h, --help Show usage\n"
" -C, --config <dir> Set path to xbps.d\n"
" -D, --distdir <dir> Set (or override) the path to void-packages\n"
" (defaults to ~/void-packages)\n"
" -d, --debug Enable debug output to stderr\n"
" -e, --removed List packages present in repos, but not in distdir\n"
" -f, --format <fmt> Output format\n"
" -I, --installed Check for outdated packages in rootdir, rather\n"
" than in the XBPS repositories\n"
" -i, --ignore-conf-repos Ignore repositories defined in xbps.d\n"
" -m, --manual Only process listed files\n"
" -R, --repository=<url> Append repository to the head of repository list\n"
" -r, --rootdir <dir> Set root directory (defaults to /)\n"
" -s, --show-all List all packages, in the format 'pkgname repover srcver'\n"
"\n [FILES...] Extra packages to process with the outdated\n"
" ones (only processed if missing).\n", prog);
return fail ? EXIT_FAILURE: EXIT_SUCCESS;
}
static void
rcv_init(rcv_t *rcv, const char *prog)
{
rcv->prog = prog;
rcv->have_vars = 0;
rcv->ptr = rcv->buf = NULL;
rcv->cache = xbps_dictionary_internalize_from_file(rcv->cachefile);
if (!rcv->cache)
rcv->cache = xbps_dictionary_create();
assert(rcv->cache);
if (rcv->xbps_conf != NULL) {
xbps_strlcpy(rcv->xhp.confdir, rcv->xbps_conf, sizeof(rcv->xhp.confdir));
}
if (rcv->rootdir != NULL) {
xbps_strlcpy(rcv->xhp.rootdir, rcv->rootdir, sizeof(rcv->xhp.rootdir));
}
if (xbps_init(&rcv->xhp) != 0)
abort();
rcv->templates = xbps_dictionary_create();
}
static void
rcv_end(rcv_t *rcv)
{
xbps_dictionary_externalize_to_file(rcv->cache, rcv->cachefile);
if (rcv->buf != NULL) {
free(rcv->buf);
rcv->buf = NULL;
}
if (rcv->env != NULL) {
xbps_object_release(rcv->env);
rcv->env = NULL;
}
xbps_end(&rcv->xhp);
if (rcv->xbps_conf != NULL)
free(rcv->xbps_conf);
if (rcv->distdir != NULL)
free(rcv->distdir);
free(rcv->cachefile);
xbps_object_release(rcv->templates);
rcv->templates = NULL;
}
static bool
rcv_load_file(rcv_t *rcv, const char *fname)
{
FILE *file;
long offset;
rcv->fname = fname;
if ((file = fopen(rcv->fname, "r")) == NULL) {
if (!rcv->manual) {
fprintf(stderr, "FileError: can't open '%s': %s\n",
rcv->fname, strerror(errno));
}
return false;
}
fseek(file, 0, SEEK_END);
offset = ftell(file);
fseek(file, 0, SEEK_SET);
if (offset == -1) {
fclose(file);
return false;
}
rcv->len = (size_t)offset;
if (rcv->buf == NULL) {
rcv->bufsz = rcv->len+1;
if (!(rcv->buf = calloc(rcv->bufsz, sizeof(char)))) {
fprintf(stderr, "MemError: can't allocate memory: %s\n",
strerror(errno));
fclose(file);
return false;
}
} else if (rcv->bufsz <= rcv->len) {
rcv->bufsz = rcv->len+1;
if (!(rcv->buf = realloc(rcv->buf, rcv->bufsz))) {
fprintf(stderr, "MemError: can't allocate memory: %s\n",
strerror(errno));
fclose(file);
return false;
}
}
(void)!fread(rcv->buf, sizeof(char), rcv->len, file);
rcv->buf[rcv->len] = '\0';
fclose(file);
rcv->ptr = rcv->buf;
return true;
}
static char *
rcv_sh_substitute(rcv_t *rcv, const char *str, size_t len)
{
const char *p;
char *cmd, *ret;
xbps_string_t out = xbps_string_create();
char b[2] = {0};
for (p = str; *p && p < str+len; p++) {
switch (*p) {
case '$':
if (p+1 < str+len) {
char buf[1024] = {0};
const char *ref;
const char *val;
size_t reflen;
p++;
if (*p == '(') {
FILE *fp;
int c;
for (ref = ++p; *p && p < str+len && *p != ')'; p++)
;
if (*p != ')')
goto err1;
cmd = strndup(ref, p-ref);
if ((fp = popen(cmd, "r")) == NULL)
goto err2;
while ((c = fgetc(fp)) != EOF && c != '\n') {
*b = c;
xbps_string_append_cstring(out, b);
}
if (pclose(fp) != 0)
goto err2;
free(cmd);
cmd = NULL;
continue;
} else if (*p == '{') {
for (ref = ++p; *p && p < str+len && (isalnum(*p) || *p == '_'); p++)
;
reflen = p-ref;
switch (*p) {
case '/': /* fallthrough */
case '%': /* fallthrough */
case '#': /* fallthrough */
case ':':
for (; *p && p < str+len && *p != '}'; p++)
;
if (*p != '}')
goto err1;
break;
case '}':
break;
default:
goto err1;
}
} else {
for (ref = p; *p && p < str+len && (isalnum(*p) || *p == '_'); p++)
;
reflen = p-ref;
p--;
}
if (reflen) {
if (reflen >= sizeof buf) {
fprintf(stderr, "out of memory\n");
exit(1);
}
strncpy(buf, ref, reflen);
if (xbps_dictionary_get_cstring_nocopy(rcv->env, buf, &val))
xbps_string_append_cstring(out, val);
else
xbps_string_append_cstring(out, "NULL");
break;
}
}
/* fallthrough */
default:
*b = *p;
xbps_string_append_cstring(out, b);
}
}
ret = xbps_string_cstring(out);
xbps_object_release(out);
return ret;
err1:
fprintf(stderr, "syntax error: in file '%s'\n", rcv->fname);
exit(1);
err2:
fprintf(stderr,
"Shell cmd failed: '%s' for "
"template '%s'",
cmd, rcv->fname);
if (errno > 0) {
fprintf(stderr, ": %s\n",
strerror(errno));
} else {
fputc('\n', stderr);
}
exit(1);
}
static void
rcv_get_pkgver(rcv_t *rcv)
{
size_t klen, vlen;
char c, *ptr = rcv->ptr, *e, *p, *k, *v, *comment, *d, *key, *val;
uint8_t vars = 0;
while ((c = *ptr) != '\0') {
if (c == '#' || c == '.') {
goto nextline;
}
if (c == '\n') {
ptr++;
continue;
}
if (c == 'u' && (strncmp("unset", ptr, 5)) == 0) {
goto nextline;
}
if ((e = strchr(ptr, '=')) == NULL)
goto nextline;
p = strchr(ptr, '\n');
k = ptr;
v = e + 1;
assert(p);
assert(k);
assert(v);
klen = strlen(k) - strlen(e);
vlen = strlen(v) - strlen(p);
if (v[0] == '"' && vlen == 1) {
while (*ptr++ != '"')
;
goto nextline;
}
if (v[0] == '"') {
v++;
vlen--;
}
if (v[vlen-1] == '"') {
vlen--;
}
comment = strchr(v, '#');
if (comment && comment < p && (comment > v && comment[-1] == ' ')) {
while (v[vlen-1] != '#') {
vlen--;
}
vlen--;
while (isspace(v[vlen-1])) {
vlen--;
}
}
if (vlen == 0) {
goto nextline;
}
key = strndup(k, klen);
if ((d = strchr(v, '$')) && d < v+vlen)
val = rcv_sh_substitute(rcv, v, vlen);
else
val = strndup(v, vlen);
if (!xbps_dictionary_set(rcv->env, key,
xbps_string_create_cstring(val))) {
fprintf(stderr, "error: xbps_dictionary_set");
exit(1);
}
if (rcv->xhp.flags & XBPS_FLAG_DEBUG)
fprintf(stderr, "%s: %s %s\n", rcv->fname, key, val);
free(key);
free(val);
if (strncmp("pkgname", k, klen) == 0) {
rcv->have_vars |= GOT_PKGNAME_VAR;
vars++;
} else if (strncmp("version", k, klen) == 0) {
rcv->have_vars |= GOT_VERSION_VAR;
vars++;
} else if (strncmp("revision", k, klen) == 0) {
rcv->have_vars |= GOT_REVISION_VAR;
vars++;
}
if (vars > 2)
return;
nextline:
ptr = strchr(ptr, '\n') + 1;
}
}
static int
rcv_process_file(rcv_t *rcv, const char *fname, rcv_check_func check)
{
int rv = 0;
xbps_object_t keysym;
xbps_object_iterator_t iter;
xbps_dictionary_t d;
xbps_data_t mtime;
const char *pkgname, *version, *revision, *reverts;
struct stat st;
bool allocenv = false;
if (stat(fname, &st) == -1) {
rv = EXIT_FAILURE;
goto ret;
}
if ((d = xbps_dictionary_get(rcv->cache, fname))) {
mtime = xbps_dictionary_get(d, "mtime");
if (!xbps_data_equals_data(mtime, &st.st_mtim, sizeof st.st_mtim))
goto update;
rcv->env = d;
rcv->have_vars = GOT_PKGNAME_VAR | GOT_VERSION_VAR | GOT_REVISION_VAR;
rcv->fname = fname;
} else {
update:
if (!rcv_load_file(rcv, fname)) {
rv = EXIT_FAILURE;
goto ret;
}
assert(rcv);
rcv->env = xbps_dictionary_create();
assert(rcv->env);
allocenv = true;
rcv_get_pkgver(rcv);
if (!xbps_dictionary_get_cstring_nocopy(rcv->env, "pkgname", &pkgname) ||
!xbps_dictionary_get_cstring_nocopy(rcv->env, "version", &version) ||
!xbps_dictionary_get_cstring_nocopy(rcv->env, "revision", &revision)) {
fprintf(stderr, "ERROR: '%s':"
" missing required variable (pkgname, version or revision)!",
fname);
exit(1);
}
if (!d) {
d = xbps_dictionary_create();
xbps_dictionary_set(rcv->cache, fname, d);
}
xbps_dictionary_set_cstring(d, "pkgname", pkgname);
xbps_dictionary_set_cstring(d, "version", version);
xbps_dictionary_set_cstring(d, "revision", revision);
reverts = NULL;
xbps_dictionary_get_cstring_nocopy(rcv->env, "reverts", &reverts);
if (reverts)
xbps_dictionary_set_cstring(d, "reverts", reverts);
mtime = xbps_data_create_data(&st.st_mtim, sizeof st.st_mtim);
xbps_dictionary_set(d, "mtime", mtime);
}
check(rcv);
ret:
if (allocenv) {
iter = xbps_dictionary_iterator(rcv->env);
while ((keysym = xbps_object_iterator_next(iter)))
xbps_object_release(xbps_dictionary_get_keysym(rcv->env, keysym));
xbps_object_iterator_release(iter);
xbps_object_release(rcv->env);
}
rcv->env = NULL;
return rv;
}
static bool
check_reverts(const char *repover, const char *reverts)
{
const char *s, *e;
size_t len;
assert(reverts);
s = reverts;
if ((len = strlen(s)) == 0)
return false;
for (s = reverts; s < reverts+len; s = e+1) {
if (!(e = strchr(s, ' ')))
e = reverts+len;
if (strncmp(s, repover, e-s) == 0)
return true;
}
return false;
}
static void
rcv_printf(rcv_t *rcv, FILE *fp, const char *pkgname, const char *repover,
const char *srcver, const char *repourl)
{
const char *f, *p;
for (f = rcv->format; *f; f++) {
if (*f == '\\') {
f++;
switch (*f) {
case '\n': fputc('\n', fp); break;
case '\t': fputc('\t', fp); break;
case '\0': fputc('\0', fp); break;
default:
fputc('\\', fp);
fputc(*f, fp);
break;
}
}
if (*f != '%') {
fputc(*f, fp);
continue;
}
switch (*++f) {
case '%': fputc(*f, fp); break;
case 'n': fputs(pkgname, fp); break;
case 'r': fputs(repover, fp); break;
case 'R': fputs(repourl, fp); break;
case 's': fputs(srcver, fp); break;
case 't':
p = strchr(rcv->fname, '/');
fwrite(rcv->fname, p ? (size_t)(p - rcv->fname) : strlen(rcv->fname), 1, fp);
break;
}
}
fputc('\n', fp);
}
static int
rcv_check_version(rcv_t *rcv)
{
const char *repover = NULL;
char srcver[BUFSIZ] = { '\0' }, *binpkgname = NULL, *s = NULL;
const char *pkgname, *version, *revision, *reverts, *repourl;
int sz;
size_t len;
assert(rcv);
if ((rcv->have_vars & GOT_PKGNAME_VAR) == 0) {
fprintf(stderr, "ERROR: '%s': missing pkgname variable!\n", rcv->fname);
exit(EXIT_FAILURE);
}
if ((rcv->have_vars & GOT_VERSION_VAR) == 0) {
fprintf(stderr, "ERROR: '%s': missing version variable!\n", rcv->fname);
exit(EXIT_FAILURE);
}
if ((rcv->have_vars & GOT_REVISION_VAR) == 0) {
fprintf(stderr, "ERROR: '%s': missing revision variable!\n", rcv->fname);
exit(EXIT_FAILURE);
}
if (!xbps_dictionary_get_cstring_nocopy(rcv->env, "pkgname", &pkgname) ||
!xbps_dictionary_get_cstring_nocopy(rcv->env, "version", &version) ||
!xbps_dictionary_get_cstring_nocopy(rcv->env, "revision", &revision)) {
fprintf(stderr, "error:\n");
exit(1);
}
reverts = NULL;
xbps_dictionary_get_cstring_nocopy(rcv->env, "reverts", &reverts);
if (rcv->removed)
sz = snprintf(srcver, sizeof srcver, "?");
else
sz = snprintf(srcver, sizeof srcver, "%s_%s", version, revision);
if (sz < 0 || (size_t)sz >= sizeof srcver)
exit(EXIT_FAILURE);
/* Check against binpkg's pkgname, not pkgname from template */
s = strchr(rcv->fname, '/');
len = s ? strlen(rcv->fname) - strlen(s) : strlen(rcv->fname);
binpkgname = strndup(rcv->fname, len);
assert(binpkgname);
repourl = NULL;
if (rcv->installed) {
rcv->pkgd = xbps_pkgdb_get_pkg(&rcv->xhp, binpkgname);
} else {
rcv->pkgd = xbps_rpool_get_pkg(&rcv->xhp, binpkgname);
xbps_dictionary_get_cstring_nocopy(rcv->pkgd, "repository", &repourl);
}
xbps_dictionary_get_cstring_nocopy(rcv->pkgd, "pkgver", &repover);
if (repover)
repover += strlen(binpkgname)+1;
free(binpkgname);
if (!repover && rcv->manual)
;
else if (rcv->show_all)
;
else if (rcv->show_removed && rcv->removed)
;
else if (rcv->show_removed && !rcv->removed)
return 0;
else if (repover && (xbps_cmpver(repover, srcver) < 0 ||
(reverts && check_reverts(repover, reverts))))
;
else
return 0;
repover = repover ? repover : "?";
repourl = repourl ? repourl : "?";
rcv_printf(rcv, stdout, pkgname, repover, srcver, repourl);
return 0;
}
static int
rcv_process_dir(rcv_t *rcv, rcv_proc_func process)
{
DIR *dir = NULL;
struct dirent entry, *result;
struct stat st;
char filename[BUFSIZ];
int i, ret = 0;
if (!(dir = opendir(".")))
goto error;
for (;;) {
i = readdir_r(dir, &entry, &result);
if (i > 0)
goto error;
if (result == NULL)
break;
if ((strcmp(result->d_name, ".") == 0) ||
(strcmp(result->d_name, "..") == 0))
continue;
if ((lstat(result->d_name, &st)) != 0)
goto error;
if (rcv->show_removed)
xbps_dictionary_set_bool(rcv->templates, result->d_name, true);
if (S_ISLNK(st.st_mode) != 0)
continue;
snprintf(filename, sizeof(filename), "%s/template", result->d_name);
ret = process(rcv, filename, rcv_check_version);
}
if ((closedir(dir)) != -1)
return ret;
error:
fprintf(stderr, "Error: while processing dir '%s/srcpkgs': %s\n",
rcv->distdir, strerror(errno));
exit(1);
}
static int
template_removed_cb(struct xbps_handle *xhp UNUSED,
xbps_object_t obj UNUSED,
const char *key,
void *arg,
bool *done UNUSED)
{
char *pkgname;
char *last_dash;
bool dummy_bool = false;
rcv_t *rcv = arg;
last_dash = strrchr(key, '-');
if (last_dash && (strcmp(last_dash, "-32bit") == 0 || strcmp(last_dash, "-dbg") == 0)) {
pkgname = strndup(key, last_dash - key);
} else {
pkgname = strdup(key);
}
if (!xbps_dictionary_get_bool(rcv->templates, pkgname, &dummy_bool)) {
rcv->removed = true;
rcv->env = xbps_dictionary_create();
xbps_dictionary_set_cstring_nocopy(rcv->env, "pkgname", key);
xbps_dictionary_set_cstring_nocopy(rcv->env, "version", "-");
xbps_dictionary_set_cstring_nocopy(rcv->env, "revision", "-");
rcv->have_vars = GOT_PKGNAME_VAR | GOT_VERSION_VAR | GOT_REVISION_VAR;
rcv->fname = key;
rcv_check_version(rcv);
rcv->removed = false;
xbps_object_release(rcv->env);
rcv->env = NULL;
}
free(pkgname);
return 0;
}
static int
repo_templates_removed_cb(struct xbps_repo *repo, void *arg, bool *done UNUSED)
{
xbps_array_t allkeys;
allkeys = xbps_dictionary_all_keys(repo->idx);
xbps_array_foreach_cb(repo->xhp, allkeys, repo->idx, template_removed_cb, arg);
xbps_object_release(allkeys);
return 0;
}
int
main(int argc, char **argv)
{
int i, c;
rcv_t rcv;
const char *prog = argv[0], *sopts = "hC:D:def:iImR:r:sV";
const struct option lopts[] = {
{ "help", no_argument, NULL, 'h' },
{ "config", required_argument, NULL, 'C' },
{ "distdir", required_argument, NULL, 'D' },
{ "removed", no_argument, NULL, 'e' },
{ "debug", no_argument, NULL, 'd' },
{ "format", required_argument, NULL, 'f' },
{ "installed", no_argument, NULL, 'I' },
{ "ignore-conf-repos", no_argument, NULL, 'i' },
{ "manual", no_argument, NULL, 'm' },
{ "repository", required_argument, NULL, 'R' },
{ "rootdir", required_argument, NULL, 'r' },
{ "show-all", no_argument, NULL, 's' },
{ "version", no_argument, NULL, 'V' },
{ NULL, 0, NULL, 0 }
};
memset(&rcv, 0, sizeof(rcv_t));
rcv.manual = false;
rcv.format = "%n %r %s %t %R";
while ((c = getopt_long(argc, argv, sopts, lopts, NULL)) != -1) {
switch (c) {
case 'h':
return show_usage(prog, false);
case 'C':
rcv.xbps_conf = xstrdup(optarg);
break;
case 'D':
rcv.distdir = xstrdup(optarg);
break;
case 'd':
rcv.xhp.flags |= XBPS_FLAG_DEBUG;
break;
case 'e':
rcv.show_removed = true;
break;
case 'f':
rcv.format = optarg;
break;
case 'i':
rcv.xhp.flags |= XBPS_FLAG_IGNORE_CONF_REPOS;
break;
case 'I':
rcv.installed = true;
break;
case 'm':
rcv.manual = true;
break;
case 'R':
xbps_repo_store(&rcv.xhp, optarg);
break;
case 'r':
rcv.rootdir = optarg;
break;
case 's':
rcv.show_all = true;
break;
case 'V':
printf("%s\n", XBPS_RELVER);
exit(EXIT_SUCCESS);
case '?':
default:
return show_usage(prog, true);
}
}
/*
* If --distdir not set default to ~/void-packages.
*/
if (rcv.distdir == NULL)
rcv.distdir = xbps_xasprintf("%s/void-packages", getenv("HOME"));
{
char *tmp = rcv.distdir;
rcv.distdir = realpath(tmp, NULL);
if (rcv.distdir == NULL) {
fprintf(stderr, "Error: realpath(%s): %s\n", tmp, strerror(errno));
exit(1);
}
free(tmp);
}
rcv.cachefile = xbps_xasprintf("%s/.xbps-checkvers-0.58.plist", rcv.distdir);
argc -= optind;
argv += optind;
rcv_init(&rcv, prog);
if (chdir(rcv.distdir) == -1 || chdir("srcpkgs") == -1) {
fprintf(stderr, "Error: while changing directory to '%s/srcpkgs': %s\n",
rcv.distdir, strerror(errno));
exit(1);
}
if (!rcv.manual)
rcv_process_dir(&rcv, rcv_process_file);
if (rcv.show_removed) {
if (rcv.installed) {
xbps_pkgdb_foreach_cb(&rcv.xhp, template_removed_cb, &rcv);
} else {
xbps_rpool_foreach(&rcv.xhp, repo_templates_removed_cb, &rcv);
}
}
rcv.manual = true;
for (i = 0; i < argc; i++) {
char tmp[PATH_MAX] = {0}, *tmpl, *p;
if (strncmp(argv[i], "srcpkgs/", sizeof ("srcpkgs/")-1) == 0) {
argv[i] += sizeof ("srcpkgs/")-1;
}
if ((p = strrchr(argv[i], '/')) && (strcmp(p, "/template")) == 0) {
tmpl = argv[i];
} else {
xbps_strlcat(tmp, argv[i], sizeof tmp);
xbps_strlcat(tmp, "/template", sizeof tmp);
tmpl = tmp;
}
rcv_process_file(&rcv, tmpl, rcv_check_version);
}
rcv_end(&rcv);
exit(EXIT_SUCCESS);
}