diff --git a/bin/xbps-query/defs.h b/bin/xbps-query/defs.h index 2a1747253..72fa5500c 100644 --- a/bin/xbps-query/defs.h +++ b/bin/xbps-query/defs.h @@ -52,16 +52,10 @@ int repo_show_pkg_namedesc(struct xbps_handle *, xbps_object_t, void *, int ownedby(struct xbps_handle *, const char *, bool, bool); /* From list.c */ -unsigned int find_longest_pkgver(struct xbps_handle *, xbps_object_t); - -int list_pkgs_in_dict(struct xbps_handle *, xbps_object_t, const char *, void *, bool *); int list_manual_pkgs(struct xbps_handle *, xbps_object_t, const char *, void *, bool *); -int list_hold_pkgs(struct xbps_handle *, xbps_object_t, const char *, void *, bool *); -int list_repolock_pkgs(struct xbps_handle *, xbps_object_t, const char *, void *, bool *); -int list_orphans(struct xbps_handle *); +int list_orphans(struct xbps_handle *, const char *); int list_pkgs_pkgdb(struct xbps_handle *); - -int repo_list(struct xbps_handle *); +int list_pkgdb(struct xbps_handle *, int (*filter)(xbps_object_t), const char *format, int json); /* from search.c */ int search(struct xbps_handle *, bool, const char *, const char *, bool); diff --git a/bin/xbps-query/list.c b/bin/xbps-query/list.c index c22ae00de..6fda565ee 100644 --- a/bin/xbps-query/list.c +++ b/bin/xbps-query/list.c @@ -24,29 +24,63 @@ */ #include -#include + +#include +#include +#include +#include #include +#include #include -#include #include -#include + +#include + +#include "xbps/fmt.h" +#include "xbps/json.h" #include "defs.h" +struct length_max_cb { + const char *key; + int max; +}; + +static int +length_max_cb(struct xbps_handle *xhp UNUSED, xbps_object_t obj, + const char *key UNUSED, void *arg, bool *loop_done UNUSED) +{ + struct length_max_cb *ctx = arg; + const char *s = NULL; + size_t len; + + if (!xbps_dictionary_get_cstring_nocopy(obj, ctx->key, &s)) + return -errno; + + len = strlen(s); + if (len > INT_MAX) + return -ERANGE; + if ((int)len > ctx->max) + ctx->max = len; + + return 0; +} + struct list_pkgver_cb { - unsigned int pkgver_len; + unsigned int pkgver_align; unsigned int maxcols; - char *linebuf; + char *buf; + struct xbps_fmt *fmt; }; -int -list_pkgs_in_dict(struct xbps_handle *xhp UNUSED, +static int +list_pkgs_pkgdb_cb(struct xbps_handle *xhp UNUSED, xbps_object_t obj, const char *key UNUSED, void *arg, bool *loop_done UNUSED) { - struct list_pkgver_cb *lpc = arg; + struct list_pkgver_cb *ctx = arg; const char *pkgver = NULL, *short_desc = NULL, *state_str = NULL; unsigned int len; pkg_state_t state; @@ -58,222 +92,198 @@ list_pkgs_in_dict(struct xbps_handle *xhp UNUSED, xbps_pkg_state_dictionary(obj, &state); - if (state == XBPS_PKG_STATE_INSTALLED) - state_str = "ii"; - else if (state == XBPS_PKG_STATE_UNPACKED) - state_str = "uu"; - else if (state == XBPS_PKG_STATE_HALF_REMOVED) - state_str = "hr"; - else - state_str = "??"; - - if (lpc->linebuf == NULL) { - printf("%s %-*s %s\n", - state_str, - lpc->pkgver_len, pkgver, - short_desc); + switch (state) { + case XBPS_PKG_STATE_INSTALLED: state_str = "ii"; break; + case XBPS_PKG_STATE_UNPACKED: state_str = "uu"; break; + case XBPS_PKG_STATE_HALF_REMOVED: state_str = "hr"; break; + case XBPS_PKG_STATE_BROKEN: state_str = "br"; break; + case XBPS_PKG_STATE_NOT_INSTALLED: state_str = "??"; break; + } + + if (!ctx->buf) { + printf("%s %-*s %s\n", state_str, ctx->pkgver_align, pkgver, + short_desc); return 0; } - len = snprintf(lpc->linebuf, lpc->maxcols, "%s %-*s %s", - state_str, - lpc->pkgver_len, pkgver, - short_desc); /* add ellipsis if the line was truncated */ - if (len >= lpc->maxcols && lpc->maxcols > 4) { - for (unsigned int j = 0; j < 3; j++) - lpc->linebuf[lpc->maxcols-j-1] = '.'; - lpc->linebuf[lpc->maxcols] = '\0'; - } - puts(lpc->linebuf); + len = snprintf(ctx->buf, ctx->maxcols, "%s %-*s %s\n", state_str, + ctx->pkgver_align, pkgver, short_desc); + if (len >= ctx->maxcols && ctx->maxcols > sizeof("...")) + memcpy(ctx->buf + ctx->maxcols - sizeof("..."), "...", sizeof("...")); + fputs(ctx->buf, stdout); return 0; } int -list_manual_pkgs(struct xbps_handle *xhp UNUSED, - xbps_object_t obj, - const char *key UNUSED, - void *arg UNUSED, - bool *loop_done UNUSED) +list_pkgs_pkgdb(struct xbps_handle *xhp) { - const char *pkgver = NULL; - bool automatic = false; - - xbps_dictionary_get_bool(obj, "automatic-install", &automatic); - if (automatic == false) { - xbps_dictionary_get_cstring_nocopy(obj, "pkgver", &pkgver); - puts(pkgver); - } + struct length_max_cb longest = {.key = "pkgver"}; + struct list_pkgver_cb lpc = {0}; - return 0; -} - -int -list_hold_pkgs(struct xbps_handle *xhp UNUSED, - xbps_object_t obj, - const char *key UNUSED, - void *arg UNUSED, - bool *loop_done UNUSED) -{ - const char *pkgver = NULL; + int r = xbps_pkgdb_foreach_cb_multi(xhp, length_max_cb, &longest); + if (r < 0) + return r; - if (xbps_dictionary_get(obj, "hold")) { - xbps_dictionary_get_cstring_nocopy(obj, "pkgver", &pkgver); - puts(pkgver); + lpc.pkgver_align = longest.max; + lpc.maxcols = get_maxcols(); + if (lpc.maxcols > 0) { + lpc.buf = malloc(lpc.maxcols); + if (!lpc.buf) + return -errno; } - return 0; + return xbps_pkgdb_foreach_cb(xhp, list_pkgs_pkgdb_cb, &lpc); } -int -list_repolock_pkgs(struct xbps_handle *xhp UNUSED, - xbps_object_t obj, - const char *key UNUSED, - void *arg UNUSED, - bool *loop_done UNUSED) -{ - const char *pkgver = NULL; - - if (xbps_dictionary_get(obj, "repolock")) { - xbps_dictionary_get_cstring_nocopy(obj, "pkgver", &pkgver); - puts(pkgver); - } - - return 0; -} +struct list_pkgdb_ctx { + struct xbps_fmt *fmt; + struct xbps_json_printer *json; + int (*filter)(xbps_object_t obj); +}; -int -list_orphans(struct xbps_handle *xhp) +static int +fmt_pkg_cb(FILE *fp, const struct xbps_fmt_var *var, void *data) { - xbps_array_t orphans; const char *pkgver = NULL; + xbps_dictionary_t pkgd = data; + xbps_object_t obj; - orphans = xbps_find_pkg_orphans(xhp, NULL); - if (orphans == NULL) - return EINVAL; + obj = xbps_dictionary_get(pkgd, var->name); + if (obj) + return xbps_fmt_print_object(var, obj, fp); - for (unsigned int i = 0; i < xbps_array_count(orphans); i++) { - xbps_dictionary_get_cstring_nocopy(xbps_array_get(orphans, i), - "pkgver", &pkgver); - puts(pkgver); + if (!xbps_dictionary_get_cstring_nocopy(pkgd, "pkgver", &pkgver)) { + xbps_error_printf("invalid package: missing `pkgver`\n"); + return -EINVAL; } - return 0; -} - -int -list_pkgs_pkgdb(struct xbps_handle *xhp) -{ - struct list_pkgver_cb lpc; - - lpc.pkgver_len = find_longest_pkgver(xhp, NULL); - lpc.maxcols = get_maxcols(); - lpc.linebuf = NULL; - if (lpc.maxcols > 0) { - lpc.linebuf = malloc(lpc.maxcols); - if (lpc.linebuf == NULL) - exit(1); + if (strcmp(var->name, "pkgname") == 0) { + char pkgname[XBPS_NAME_SIZE]; + if (xbps_pkg_name(pkgname, sizeof(pkgname), pkgver)) { + xbps_error_printf("invalid `pkgver`: %s\n", pkgver); + return -EINVAL; + } + return xbps_fmt_print_string(var, pkgname, 0, fp); + } else if (strcmp(var->name, "version") == 0) { + const char *version = xbps_pkg_version(pkgver); + if (!version) { + xbps_error_printf("invalid `pkgver`: %s\n", pkgver); + return -EINVAL; + } + return xbps_fmt_print_string(var, version, 0, fp); + } else if (strcmp(var->name, "revision") == 0) { + const char *revision = xbps_pkg_revision(pkgver); + if (!revision) { + xbps_error_printf("invalid `pkgver`: %s\n", pkgver); + return -EINVAL; + } + return xbps_fmt_print_string(var, revision, 0, fp); } - return xbps_pkgdb_foreach_cb(xhp, list_pkgs_in_dict, &lpc); + return xbps_fmt_print_object(var, NULL, fp); } -static void -repo_list_uri(struct xbps_repo *repo) +static int +list_pkgdb_cb(struct xbps_handle *xhp UNUSED, xbps_object_t obj, + const char *key UNUSED, void *arg, bool *loop_done UNUSED) { - const char *signedby = NULL; - uint16_t pubkeysize = 0; - - printf("%5zd %s", - repo->idx ? (ssize_t)xbps_dictionary_count(repo->idx) : -1, - repo->uri); - printf(" (RSA %s)\n", repo->is_signed ? "signed" : "unsigned"); - if (repo->xhp->flags & XBPS_FLAG_VERBOSE) { - xbps_data_t pubkey; - char *hexfp = NULL; - - xbps_dictionary_get_cstring_nocopy(repo->idxmeta, "signature-by", &signedby); - xbps_dictionary_get_uint16(repo->idxmeta, "public-key-size", &pubkeysize); - pubkey = xbps_dictionary_get(repo->idxmeta, "public-key"); - if (pubkey) - hexfp = xbps_pubkey2fp(pubkey); - if (signedby) - printf(" Signed-by: %s\n", signedby); - if (pubkeysize && hexfp) - printf(" %u %s\n", pubkeysize, hexfp); - if (hexfp) - free(hexfp); + struct list_pkgdb_ctx *ctx = arg; + int r; + + if (ctx->filter) { + r = ctx->filter(obj); + if (r <= 0) + return r; } -} -static void -repo_list_uri_err(const char *repouri) -{ - printf("%5zd %s (RSA maybe-signed)\n", (ssize_t) -1, repouri); + if (ctx->fmt) { + r = xbps_fmt(ctx->fmt, &fmt_pkg_cb, obj, stdout); + } else if (ctx->json) { + r = xbps_json_print_xbps_object(ctx->json, obj); + if (r < 0) + return r; + fprintf(ctx->json->file, "\n"); + } else { + r = -ENOTSUP; + } + return r; } int -repo_list(struct xbps_handle *xhp) +list_pkgdb(struct xbps_handle *xhp, int (*filter)(xbps_object_t), const char *format, int json) { - for (unsigned int i = 0; i < xbps_array_count(xhp->repositories); i++) { - const char *repouri = NULL; - struct xbps_repo *repo; - xbps_array_get_cstring_nocopy(xhp->repositories, i, &repouri); - repo = xbps_repo_open(xhp, repouri); - if (!repo) { - repo_list_uri_err(repouri); - continue; + struct list_pkgdb_ctx ctx = {.filter = filter}; + struct xbps_json_printer pr = {0}; + int r; + if (json > 0) { + pr.indent = (json-1) * 2; + pr.compact = pr.indent == 0; + pr.file = stdout; + ctx.json = ≺ + } else if (format) { + ctx.fmt = xbps_fmt_parse(format); + if (!ctx.fmt) { + r = -errno; + xbps_error_printf("failed to parse format: %s\n", strerror(-r)); + return r; } - repo_list_uri(repo); - xbps_repo_release(repo); } - return 0; + r = xbps_pkgdb_foreach_cb(xhp, list_pkgdb_cb, &ctx); + xbps_fmt_free(ctx.fmt); + return r; } -struct fflongest { - xbps_dictionary_t d; - unsigned int len; -}; - -static int -_find_longest_pkgver_cb(struct xbps_handle *xhp UNUSED, - xbps_object_t obj, - const char *key UNUSED, - void *arg, - bool *loop_done UNUSED) +int +list_manual_pkgs(struct xbps_handle *xhp UNUSED, + xbps_object_t obj, + const char *key UNUSED, + void *arg UNUSED, + bool *loop_done UNUSED) { - struct fflongest *ffl = arg; const char *pkgver = NULL; - unsigned int len; + bool automatic = false; - xbps_dictionary_get_cstring_nocopy(obj, "pkgver", &pkgver); - len = strlen(pkgver); - if (ffl->len == 0 || len > ffl->len) - ffl->len = len; + xbps_dictionary_get_bool(obj, "automatic-install", &automatic); + if (automatic == false) { + xbps_dictionary_get_cstring_nocopy(obj, "pkgver", &pkgver); + puts(pkgver); + } return 0; } -unsigned int -find_longest_pkgver(struct xbps_handle *xhp, xbps_object_t o) +int +list_orphans(struct xbps_handle *xhp, const char *format) { - struct fflongest ffl; - - ffl.d = o; - ffl.len = 0; - - if (xbps_object_type(o) == XBPS_TYPE_DICTIONARY) { - xbps_array_t array; + xbps_array_t orphans; + struct xbps_fmt *fmt; + int r = 0; + + fmt = xbps_fmt_parse(format); + if (!fmt) { + r = -errno; + xbps_error_printf("failed to parse format: %s\n", strerror(-r)); + return r; + } - array = xbps_dictionary_all_keys(o); - (void)xbps_array_foreach_cb_multi(xhp, array, o, - _find_longest_pkgver_cb, &ffl); - xbps_object_release(array); - } else { - (void)xbps_pkgdb_foreach_cb_multi(xhp, - _find_longest_pkgver_cb, &ffl); + orphans = xbps_find_pkg_orphans(xhp, NULL); + if (!orphans) { + r = -errno; + xbps_error_printf("failed to find orphans: %s\n", strerror(-r)); + goto err; } - return ffl.len; + for (unsigned int i = 0; i < xbps_array_count(orphans); i++) { + xbps_object_t obj = xbps_array_get(orphans, i); + if (!obj) + return -errno; + r = xbps_fmt(fmt, &fmt_pkg_cb, obj, stdout); + if (r < 0) + goto err; + } +err: + xbps_fmt_free(fmt); + return r; } diff --git a/bin/xbps-query/main.c b/bin/xbps-query/main.c index 44316c1ad..feed6891c 100644 --- a/bin/xbps-query/main.c +++ b/bin/xbps-query/main.c @@ -23,13 +23,18 @@ * THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. */ +#include +#include +#include #include #include #include -#include -#include #include + +#include "xbps/fmt.h" +#include "xbps/json.h" + #include "defs.h" static void __attribute__((noreturn)) @@ -41,8 +46,10 @@ usage(bool fail) " -C, --config Path to confdir (xbps.d)\n" " -c, --cachedir Path to cachedir\n" " -d, --debug Debug mode shown to stderr\n" + " -F, --format Format for list output\n" " -h, --help Show usage\n" " -i, --ignore-conf-repos Ignore repositories defined in xbps.d\n" + " -J, --json Print output as json\n" " -M, --memory-sync Remote repository data is fetched and stored\n" " in memory, ignoring on-disk repodata archives\n" " -p, --property PROP[,...] Show properties for PKGNAME\n" @@ -74,16 +81,113 @@ usage(bool fail) exit(fail ? EXIT_FAILURE : EXIT_SUCCESS); } +static int +repo_fmt(FILE *fp, const struct xbps_fmt_var *var, void *data) +{ + struct xbps_repo *repo = data; + if (strcmp(var->name, "url") == 0) { + return xbps_fmt_print_string(var, repo->uri, 0, fp); + } else if (strcmp(var->name, "signed") == 0) { + return xbps_fmt_print_string(var, repo->is_signed ? "true" : "false", 0, fp); + } else if (strcmp(var->name, "packages") == 0) { + return xbps_fmt_print_number(var, xbps_dictionary_count(repo->idx), fp); + } + return 0; +} + +static void +show_repo(struct xbps_repo *repo) +{ + xbps_data_t pubkey; + const char *signedby = NULL; + char *hexfp = NULL; + uint16_t pubkeysize = 0; + + printf("%5zd %s", + repo->idx ? (ssize_t)xbps_dictionary_count(repo->idx) : -1, + repo->uri); + printf(" (RSA %s)\n", repo->is_signed ? "signed" : "unsigned"); + if (!(repo->xhp->flags & XBPS_FLAG_VERBOSE)) + return; + + xbps_dictionary_get_cstring_nocopy(repo->idxmeta, "signature-by", &signedby); + xbps_dictionary_get_uint16(repo->idxmeta, "public-key-size", &pubkeysize); + pubkey = xbps_dictionary_get(repo->idxmeta, "public-key"); + if (pubkey) + hexfp = xbps_pubkey2fp(pubkey); + if (signedby) + printf(" Signed-by: %s\n", signedby); + if (pubkeysize && hexfp) + printf(" %u %s\n", pubkeysize, hexfp); + free(hexfp); +} + +static int +show_repos(struct xbps_handle *xhp, const char *format) +{ + struct xbps_fmt *fmt = NULL; + if (format) { + fmt = xbps_fmt_parse(format); + if (!fmt) { + xbps_error_printf("failed to parse format: %s\n", strerror(errno)); + return 1; + } + } + for (unsigned int i = 0; i < xbps_array_count(xhp->repositories); i++) { + const char *repouri = NULL; + struct xbps_repo *repo; + xbps_array_get_cstring_nocopy(xhp->repositories, i, &repouri); + repo = xbps_repo_open(xhp, repouri); + if (!repo) { + printf("%5zd %s (RSA maybe-signed)\n", (ssize_t)-1, repouri); + continue; + } + if (fmt) { + int r = xbps_fmt(fmt, repo_fmt, repo, stdout); + if (r < 0) { + xbps_error_printf("failed to format repo: %s\n", strerror(-r)); + return 1; + } + } else { + show_repo(repo); + } + xbps_repo_release(repo); + } + xbps_fmt_free(fmt); + return 0; +} + +static int +filter_hold(xbps_object_t obj) +{ + return xbps_dictionary_get(obj, "hold") != NULL; +} + +static int +filter_manual(xbps_object_t obj) +{ + bool automatic = false; + xbps_dictionary_get_bool(obj, "automatic-install", &automatic); + return !automatic; +} + +static int +filter_repolock(xbps_object_t obj) +{ + return xbps_dictionary_get(obj, "repolock") != NULL; +} + int main(int argc, char **argv) { - const char *shortopts = "C:c:df:hHiLlMmOo:p:Rr:s:S:VvX:x:"; + const char *shortopts = "C:c:dF:f:hHiJLlMmOo:p:Rr:s:S:VvX:x:"; const struct option longopts[] = { { "config", required_argument, NULL, 'C' }, { "cachedir", required_argument, NULL, 'c' }, { "debug", no_argument, NULL, 'd' }, { "help", no_argument, NULL, 'h' }, { "ignore-conf-repos", no_argument, NULL, 'i' }, + { "json", no_argument, NULL, 'J' }, { "list-repos", no_argument, NULL, 'L' }, { "list-pkgs", no_argument, NULL, 'l' }, { "list-hold-pkgs", no_argument, NULL, 'H' }, @@ -100,6 +204,7 @@ main(int argc, char **argv) { "version", no_argument, NULL, 'V' }, { "verbose", no_argument, NULL, 'v' }, { "files", required_argument, NULL, 'f' }, + { "format", required_argument, NULL, 'F' }, { "deps", required_argument, NULL, 'x' }, { "revdeps", required_argument, NULL, 'X' }, { "regex", no_argument, NULL, 0 }, @@ -107,68 +212,82 @@ main(int argc, char **argv) { "cat", required_argument, NULL, 2 }, { NULL, 0, NULL, 0 }, }; - struct xbps_handle xh; - const char *pkg, *rootdir, *cachedir, *confdir, *props, *catfile; - int c, flags, rv; - bool list_pkgs, list_repos, orphans, own, list_repolock; - bool list_manual, list_hold, show_prop, show_files, show_deps, show_rdeps; - bool show, pkg_search, regex, repo_mode, opmode, fulldeptree; - - rootdir = cachedir = confdir = props = pkg = catfile = NULL; - flags = rv = c = 0; - list_pkgs = list_repos = list_hold = orphans = pkg_search = own = false; - list_manual = list_repolock = show_prop = show_files = false; - regex = show = show_deps = show_rdeps = fulldeptree = false; - repo_mode = opmode = false; + struct xbps_handle xh = {0}; + const char *pkg = NULL, *props = NULL, *catfile, *format; + int c, rv; + bool regex = false, repo_mode = false, fulldeptree = false; + int json = 0; + enum { + CAT_FILE = 1, + LIST_HOLD, + LIST_INSTALLED, + LIST_MANUAL, + LIST_ORPHANS, + LIST_REPOLOCK, + SHOW_REPOS, + SEARCH_FILE, + SEARCH_PKG, + SHOW_DEPS, + SHOW_FILES, + SHOW_PKG, + SHOW_REVDEPS, + } mode = 0; - memset(&xh, 0, sizeof(xh)); + props = pkg = catfile = format = NULL; + rv = c = 0; + repo_mode = false; while ((c = getopt_long(argc, argv, shortopts, longopts, NULL)) != -1) { switch (c) { case 'C': - confdir = optarg; + xbps_strlcpy(xh.confdir, optarg, sizeof(xh.confdir)); break; case 'c': - cachedir = optarg; + xbps_strlcpy(xh.cachedir, optarg, sizeof(xh.cachedir)); break; case 'd': - flags |= XBPS_FLAG_DEBUG; + xh.flags |= XBPS_FLAG_DEBUG; break; case 'f': pkg = optarg; - show_files = opmode = true; + mode = SHOW_FILES; + break; + case 'F': + format = optarg; + break; + case 'J': + json++; break; case 'H': - list_hold = opmode = true; + mode = LIST_HOLD; break; case 'h': usage(false); /* NOTREACHED */ case 'i': - flags |= XBPS_FLAG_IGNORE_CONF_REPOS; + xh.flags |= XBPS_FLAG_IGNORE_CONF_REPOS; break; case 'L': - list_repos = opmode = true; + mode = SHOW_REPOS; break; case 'l': - list_pkgs = opmode = true; + mode = LIST_INSTALLED; break; case 'M': - flags |= XBPS_FLAG_REPOS_MEMSYNC; + xh.flags |= XBPS_FLAG_REPOS_MEMSYNC; break; case 'm': - list_manual = opmode = true; + mode = LIST_MANUAL; break; case 'O': - orphans = opmode = true; + mode = LIST_ORPHANS; break; case 'o': pkg = optarg; - own = opmode = true; + mode = SEARCH_FILE; break; case 'p': props = optarg; - show_prop = true; break; case 'R': if (optarg != NULL) { @@ -177,29 +296,29 @@ main(int argc, char **argv) repo_mode = true; break; case 'r': - rootdir = optarg; + xbps_strlcpy(xh.rootdir, optarg, sizeof(xh.rootdir)); break; case 'S': pkg = optarg; - show = opmode = true; + mode = SHOW_PKG; break; case 's': pkg = optarg; - pkg_search = opmode = true; + mode = SEARCH_PKG; break; case 'v': - flags |= XBPS_FLAG_VERBOSE; + xh.flags |= XBPS_FLAG_VERBOSE; break; case 'V': printf("%s\n", XBPS_RELVER); exit(EXIT_SUCCESS); case 'x': pkg = optarg; - show_deps = opmode = true; + mode = SHOW_DEPS; break; case 'X': pkg = optarg; - show_rdeps = opmode = true; + mode = SHOW_REVDEPS; break; case 0: regex = true; @@ -208,10 +327,11 @@ main(int argc, char **argv) fulldeptree = true; break; case 2: + mode = CAT_FILE; catfile = optarg; break; case 3: - list_repolock = opmode = true; + mode = LIST_REPOLOCK; break; case '?': default: @@ -222,98 +342,83 @@ main(int argc, char **argv) argc -= optind; argv += optind; - if (!argc && !opmode) { - usage(true); - /* NOTREACHED */ - } else if (!opmode) { - /* show mode by default */ - show = opmode = true; + /* no mode (defaults to show) and cat mode take a trailing argv */ + if (mode == 0 || mode == CAT_FILE) { + if (argc == 0) + usage(true); + if (mode == 0) + mode = SHOW_PKG; pkg = *(argv++); argc--; } - if (argc) { - /* trailing parameters */ + + /* trailing parameters */ + if (argc != 0) usage(true); - /* NOTREACHED */ - } + /* * Initialize libxbps. */ - if (rootdir) - xbps_strlcpy(xh.rootdir, rootdir, sizeof(xh.rootdir)); - if (cachedir) - xbps_strlcpy(xh.cachedir, cachedir, sizeof(xh.cachedir)); - if (confdir) - xbps_strlcpy(xh.confdir, confdir, sizeof(xh.confdir)); - - xh.flags = flags; - if ((rv = xbps_init(&xh)) != 0) { xbps_error_printf("Failed to initialize libxbps: %s\n", strerror(rv)); exit(EXIT_FAILURE); } - if (list_repos) { - /* list repositories */ - rv = repo_list(&xh); - - } else if (list_hold) { - /* list on hold pkgs */ - rv = xbps_pkgdb_foreach_cb(&xh, list_hold_pkgs, NULL); - - } else if (list_repolock) { - /* list repolocked packages */ - rv = xbps_pkgdb_foreach_cb(&xh, list_repolock_pkgs, NULL); - - } else if (list_manual) { - /* list manual pkgs */ - rv = xbps_pkgdb_foreach_cb(&xh, list_manual_pkgs, NULL); - - } else if (list_pkgs) { - /* list available pkgs */ - rv = list_pkgs_pkgdb(&xh); - - } else if (orphans) { - /* list pkg orphans */ - rv = list_orphans(&xh); - - } else if (own) { - /* ownedby mode */ + switch (mode) { + case LIST_HOLD: + rv = list_pkgdb(&xh, filter_hold, format ? format : "{pkgver}\n", json) < 0; + break; + case LIST_INSTALLED: + if (format || json > 0) { + rv = list_pkgdb(&xh, NULL, format, json); + } else { + rv = list_pkgs_pkgdb(&xh); + } + break; + case LIST_MANUAL: + rv = list_pkgdb(&xh, filter_manual, format ? format : "{pkgver}\n", json) < 0; + break; + case LIST_ORPHANS: + rv = list_orphans(&xh, format ? format : "{pkgver}\n") < 0; + break; + case LIST_REPOLOCK: + rv = list_pkgdb(&xh, filter_repolock, format ? format : "{pkgver}\n", json) < 0; + break; + case SHOW_REPOS: + rv = show_repos(&xh, format); + break; + case SEARCH_FILE: rv = ownedby(&xh, pkg, repo_mode, regex); - - } else if (pkg_search) { - /* search mode */ + break; + case SEARCH_PKG: rv = search(&xh, repo_mode, pkg, props, regex); - - } else if (catfile) { - /* repo cat file mode */ + break; + case SHOW_DEPS: + rv = show_pkg_deps(&xh, pkg, repo_mode, fulldeptree); + break; + case SHOW_FILES: if (repo_mode) - rv = repo_cat_file(&xh, pkg, catfile); + rv = repo_show_pkg_files(&xh, pkg); else - rv = cat_file(&xh, pkg, catfile); - } else if (show || show_prop) { - /* show mode */ + rv = show_pkg_files_from_metadir(&xh, pkg); + break; + case CAT_FILE: if (repo_mode) - rv = repo_show_pkg_info(&xh, pkg, props); + rv = repo_cat_file(&xh, pkg, catfile); else - rv = show_pkg_info_from_metadir(&xh, pkg, props); - - } else if (show_files) { - /* show-files mode */ + rv = cat_file(&xh, pkg, catfile); + break; + case SHOW_PKG: if (repo_mode) - rv = repo_show_pkg_files(&xh, pkg); + rv = repo_show_pkg_info(&xh, pkg, props); else - rv = show_pkg_files_from_metadir(&xh, pkg); - - } else if (show_deps) { - /* show-deps mode */ - rv = show_pkg_deps(&xh, pkg, repo_mode, fulldeptree); - - } else if (show_rdeps) { - /* show-rdeps mode */ + rv = show_pkg_info_from_metadir(&xh, pkg, props); + break; + case SHOW_REVDEPS: rv = show_pkg_revdeps(&xh, pkg, repo_mode); - } + break; + } xbps_end(&xh); exit(rv); diff --git a/bin/xbps-query/xbps-query.1 b/bin/xbps-query/xbps-query.1 index b4c364180..e931a808e 100644 --- a/bin/xbps-query/xbps-query.1 +++ b/bin/xbps-query/xbps-query.1 @@ -88,6 +88,11 @@ If the first character is not '/' then it's a relative path of .Ar rootdir . .It Fl d, Fl -debug Enables extra debugging shown to stderr. +.It Fl F, Fl -format Ar format +Format string for output formatting. +See +.Sx FORMAT STRINGS +for syntax. .It Fl h, Fl -help Show the help message. .It Fl i, Fl -ignore-conf-repos @@ -95,6 +100,23 @@ Ignore repositories defined in configuration files. Only repositories specified in the command line via .Ar --repository will be used. +.It Fl J, Fl -json +Format the output as newline-delimited JSON objects. +If the flag is specified more than once the output will be +.Dq pretty-printed +adding additional line breaks and indentions to the output. +For each repetition two levels of indention are added. +.Pp +The output for the following modes will be formatted as followed: +.Bl -tag -width -x -compact +.It Fl l , Fl -list-pkgs +.It Fl H , Fl -list-hold-pkgs +.It Fl -list-repolock-pkgs +.It Fl m , Fl -list-manual-pkgs +.It Fl O , Fl -list-orphans +JSON objects containing the package +.Sx PROPERTIES . +.El .It Fl M, Fl -memory-sync For remote repositories, the data is fetched and stored in memory for the current operation. @@ -174,6 +196,21 @@ If a repository is not available the number of packages will be The .Fl v option can be used to show more detailed information of remote repositories. +.Pp +The following +.Fl F , Fl -format +variables can be used: +.Bl -tag -compact -width Ic +.It Ic url +repository url. +.It Ic signed +.Sq true +if the repository is signed or +.Sq false +otherwise. +.It Ic packages +The number of packages in the repository. +.El .It Fl H, Fl -list-hold-pkgs List registered packages in the package database (pkgdb) that are on .Sy hold . @@ -277,6 +314,76 @@ expression wins. This expects an absolute path. This mode only works with repositories. .El +.Sh FORMAT STRINGS +Variables are package properties if not otherwise documented. +See +.Sx PROPERTIES +section for a list of available properties. +.Pp +As example a format string like: +.Bd -offset indent -literal +{pkgname:<30} {installed_size!humanize :>10}\\n +.Ed +.Pp +Would produce a list formatted like: +.Bd -offset indent -literal +libxbps 304 KB +xbps 484 KB +.Ed +.Pp +Format strings are parsed by the following EBNF: +.Bd -literal + ::= (prefix | "\\" (escape|[{}]) | substitution)* + ::= [^\\{}]+ -- Literal text chunk. + ::= [abefnrtv0] -- POSIX-like escape character. + + ::= "{" variable ["?" default] ["!" conversion] [":" format] "}" + ::= [a-zA-Z0-9_-] + + ::= ([-]?[0-9]+) -- default number. + | "true" | "false" -- default boolean. + | ('"' (("\\" (escape|'"')) | [^"])* '"') -- default string. + + ::= humanize | strmode | json + +-- Convert inode status information into a symbolic string. + ::= "strmode" + +-- Format a number into a human readable form, the default is:`humanize .8Ki`: + ::= "humanize" [space] [decimal] [width] [scale] [i] + ::= " " -- Put a space between number and the suffix. + ::= "." -- If the final result is less than 10, display + it using one digit. + ::= [0-9]+ -- Width of the output. + ::= multiplier -- Minimum scale multiplier and optionally + [multiplier] -- Maximum scale multiplier. + ::= "B" -- byte + | "K" -- kilo + | "M" -- mega + | "G" -- giga + | "T" -- tera + | "P" -- peta + | "E" -- exa + ::= "i" -- Use IEEE/IEC (and now also SI) power of two prefixes. + +-- Format value as json value. + ::= "json" + + ::= [[fill] align] [sign] [width] ["." precision] [type] + ::= -- The character to use when aligning the output. + ::= "<" -- Left align. + | ">" -- Right align. + | "=" -- Left align with zero paddings after the sign. + ::= "+" -- Add sign to positive and negative numbers. + | "-" -- Add sign to negative numbers. + ::= [0-9]+ -- The alignment width. + ::= [0-9]+ -- Precision for numbers. + ::= "d" -- Decimal number. + | "o" -- Octal number. + | "u" -- Unsigned number. + | "x" -- Hexadecimal with lowercase letters. + | "X" -- Hexadecimal with uppercase letters. +.Ed .Sh PROPERTIES This is the list of a packages properties. Note that not all properties are available for all packages. @@ -353,6 +460,16 @@ installation state of the package. .It Ic tags list of categories the package is associated with. .El +.Pp +Additional dynamic properties are available for +.Sx FORMAT STRINGS . +.Pp +.Bl -tag -compact -width 17m +.It Ic version +version of the package. +.It Ic revision +revision of the package. +.El .Sh ENVIRONMENT .Bl -tag -width XBPS_TARGET_ARCH .It Sy XBPS_ARCH diff --git a/data/repod-main.conf b/data/repod-main.conf index e1c99f1e3..d97352243 100644 --- a/data/repod-main.conf +++ b/data/repod-main.conf @@ -1 +1 @@ -repository=https://repo-default.voidlinux.org/current +repository=https://repo-default.voidlinux.org/current/{arch} diff --git a/data/xbps.d.5 b/data/xbps.d.5 index 9d2d1063e..ee172b373 100644 --- a/data/xbps.d.5 +++ b/data/xbps.d.5 @@ -111,12 +111,20 @@ argument accepts local and remote repositories. A complete url or absolute path to the directory that stores the .Em -repodata archive is expected. +If the +.Ar url +contains +.Ar {arch} , +.Ar {arch} +will be replaced by the current target XBPS architecture at runtime. +If the target architecture is not set, the native architecture will be used. Note that remote repositories must be signed using .Xr xbps-rindex 1 , example: .Pp .Bl -tag -compact -width repository=https://repo-default.voidlinux.org/current .It Sy repository=https://repo-default.voidlinux.org/current +.It Sy repository=https://repo-default.voidlinux.org/current/{arch} .It Sy repository=/hostdir/binpkgs .El .It Sy rootdir=path diff --git a/include/compat.h b/include/compat.h index cedd1be29..f33731ab0 100644 --- a/include/compat.h +++ b/include/compat.h @@ -28,13 +28,19 @@ int HIDDEN vasprintf(char **, const char *, va_list); #endif #ifndef HAVE_HUMANIZE_NUMBER -#define HN_DECIMAL 0x01 -#define HN_NOSPACE 0x02 -#define HN_B 0x04 -#define HN_DIVISOR_1000 0x08 -#define HN_GETSCALE 0x10 -#define HN_AUTOSCALE 0x20 -int HIDDEN humanize_number(char *, size_t, int64_t, const char *, int, int); +/* Values for humanize_number(3)'s flags parameter. */ +#define HN_DECIMAL 0x01 +#define HN_NOSPACE 0x02 +#define HN_B 0x04 +#define HN_DIVISOR_1000 0x08 +#define HN_IEC_PREFIXES 0x10 + +/* Values for humanize_number(3)'s scale parameter. */ +#define HN_GETSCALE 0x10 +#define HN_AUTOSCALE 0x20 + +int HIDDEN humanize_number(char *_buf, size_t _len, int64_t _number, + const char *_suffix, int _scale, int _flags); #endif #endif /* COMPAT_H */ diff --git a/include/xbps/fmt.h b/include/xbps/fmt.h new file mode 100644 index 000000000..a21dc9f95 --- /dev/null +++ b/include/xbps/fmt.h @@ -0,0 +1,256 @@ +/* SPDX-FileCopyrightText: Copyright 2023 Duncan Overbruck */ +/* SPDX-License-Identifier: BSD-2-Clause */ + +#ifndef _XBPS_FMT_H_ +#define _XBPS_FMT_H_ + +#include +#include +#include + +#include +#include + +/** @addtogroup format */ +/**@{*/ + +/** + * @struct xbps_fmt xbps/fmt.h + * @brief Structure of parsed format string. + */ +struct xbps_fmt; + +/** + * @struct xbps_fmt xbps/fmt.h + * @brief Structure of a parsed format string variable. + */ +struct xbps_fmt_var { + /** + * @var name + * @brief Variable name. + */ + char *name; + /** + * @var def + * @brief Default value. + */ + struct xbps_fmt_def *def; + /** + * @var conv + * @brief Format conversion. + */ + struct xbps_fmt_conv *conv; + /** + * @var spec + * @brief Format specification. + */ + struct xbps_fmt_spec *spec; +}; + +/** + * @struct xbps_fmt_def xbps/fmt.h + * @brief Structure of format default value. + */ +struct xbps_fmt_def { + enum { + XBPS_FMT_DEF_STR = 1, + XBPS_FMT_DEF_NUM, + XBPS_FMT_DEF_BOOL, + } type; + union { + char *str; + int64_t num; + bool boolean; + } val; +}; + +/** + * @struct xbps_fmt_spec xbps/fmt.h + * @brief Structure of parsed format specifier. + */ +struct xbps_fmt_spec { + /** + * @var fill + * @brief Padding character. + */ + char fill; + /** + * @var align + * @brief Alignment modifier. + * + * Possible values are: + * - `<`: left align. + * - `>`: right align. + * - `=`: place padding after the sign. + */ + char align; + /** + * @var sign + * @brief Sign modifier. + * + * Possible values are: + * - `-`: sign negative numbers. + * - `+`: sign both negative and positive numbers. + * - space: sign negative numbers and add space before positive numbers. + */ + char sign; + /** + * @var width + * @brief Minimum width. + */ + unsigned int width; + /** + * @var precision + * @brief Precision. + */ + unsigned int precision; + /** + * @var type + * @brief Type specifier usually to change the output format type. + * + * Can contain any character, xbps_fmt_number() uses the following: + * - `u`: Unsigned decimal. + * - `d`: Decimal. + * - `x`: Hex with lowercase letters. + * - `X`: hex with uppercase letters. + * - `h`: Human readable using humanize_number(3). + */ + char type; +}; + +/** + * @brief Format callback, called for each variable in the format string. + * + * The callback function should write data as specified by \a var to \a fp. + * + * @param[in] fp File to format to. + * @param[in] var Variable to format. + * @param[in] data Userdata passed to the xbps_fmt() function. + */ +typedef int (xbps_fmt_cb)(FILE *fp, const struct xbps_fmt_var *var, void *data); + +/** + * @brief Parses the format string \a format. + * + * @param[in] format The format string. + * + * @return The parsed format structure, or NULL on error. + * The returned buffer must be freed with xbps_fmt_free(). + * @retval EINVAL Invalid format string. + * @retval ERANGE Invalid alignment specifier. + * @retval ENOMEM Memory allocation failure. + */ +struct xbps_fmt *xbps_fmt_parse(const char *format); + +/** + * @brief Releases memory associated with \a fmt. + * + * @param[in] fmt The format string. + */ +void xbps_fmt_free(struct xbps_fmt *fmt); + +/** + * @brief Print formatted text to \a fp. + * + * @param[in] fmt Format returned by struct xbps_fmt_parse(). + * @param[in] cb Callback function called for each variable in the format. + * @param[in] data Userdata passed to the callback \a cb. + * @param[in] fp File to print to. + * + * @return 0 on success or a negative errno. + * @retval 0 Success + */ +int xbps_fmt(const struct xbps_fmt *fmt, xbps_fmt_cb *cb, void *data, FILE *fp); + +/** + * @brief Print formatted dictionary values to \a fp. + * + * Prints formatted dictionary values as specified by the parsed \a fmt + * format string to \a fp. + * + * @param[in] fmt Format returned by struct xbps_fmt_parse(). + * @param[in] dict Dictionary to print values from. + * @param[in] fp File to print to. + * + * @return 0 on success or value returned by \a cb. + * @retval 0 Success + */ +int xbps_fmt_dictionary(const struct xbps_fmt *fmt, xbps_dictionary_t dict, FILE *fp); + +/** + * @brief Print formatted dictionary values to \a fp. + * + * Prints formatted dictionary values as specified by the format string + * \a format to \a fp. + * + * @param[in] format Format string. + * @param[in] dict Dictionary to print values from. + * @param[in] fp File to print to. + * + * @return 0 on success or value returned by \a cb. + * @retval 0 Success + */ +int xbps_fmts_dictionary(const char *format, xbps_dictionary_t dict, FILE *fp); + +/** + * @brief Print formatted dictionary to \a fp. + * + * Print the formatted dictionary according to the \a format format string + * to \a fp. + * + * @param[in] format Format string. + * @param[in] cb Callback function called for each variable in the format. + * @param[in] data Userdata passed to the callback \a cb. + * @param[in] fp File to print to. + * + * @return 0 on success. + * @retval 0 Success. + * @retval -EINVAL Invalid format string. + * @retval -ERANGE Invalid alignment specifier. + * @retval -ENOMEM Memory allocation failure. + */ +int xbps_fmts(const char *format, xbps_fmt_cb *cb, void *data, FILE *fp); + +/** + * @brief Print formatted number to \a fp. + * + * Prints the number \a num to \a fp according to the specification \a var. + * + * @param[in] var Variable to format. + * @param[in] num Number to print. + * @param[in] fp File to print to. + * + * @return Returns 0 on success. + */ +int xbps_fmt_print_number(const struct xbps_fmt_var *var, int64_t num, FILE *fp); + +/** + * @brief Print formatted string to \a fp. + * + * Prints the string \a str to \a fp according to the specification \a var. + * + * @param[in] var Variable to print. + * @param[in] str String to print. + * @param[in] len Length of the string or 0. + * @param[in] fp File to print to. + * + * @return Returns 0 on success. + */ +int xbps_fmt_print_string(const struct xbps_fmt_var *var, const char *str, size_t len, FILE *fp); + +/** + * @brief Print formatted ::xbps_object_t to \a fp. + * + * Prints the ::xbps_object_t \a obj to \a fp according to the specification in \a var. + * + * @param[in] var Variable to format. + * @param[in] obj The object to print. + * @param[in] fp File to print to. + * + * @return Returns 0 on success. + */ +int xbps_fmt_print_object(const struct xbps_fmt_var *var, xbps_object_t obj, FILE *fp); + +/**@}*/ + +#endif /* !_XBPS_FMT_H_ */ diff --git a/include/xbps/json.h b/include/xbps/json.h new file mode 100644 index 000000000..b00b8c9f1 --- /dev/null +++ b/include/xbps/json.h @@ -0,0 +1,139 @@ +/* SPDX-FileCopyrightText: Copyright 2023 Duncan Overbruck */ +/* SPDX-License-Identifier: BSD-2-Clause */ + +#ifndef _XBPS_JSON_H_ +#define _XBPS_JSON_H_ + +#include +#include + +#include +#include +#include +#include +#include +#include + +/** @addtogroup json */ +/**@{*/ + +/** + * @struct xbps_json_printer xbps/json.h + * @brief Structure holding state while printing json. + */ +struct xbps_json_printer { + /** + * @var file + * @brief Output file to print to. + */ + FILE *file; + /** + * @var depth + * @brief The current depth inside objects or arrays. + */ + unsigned depth; + /** + * @var indent + * @brief Number of indent spaces per depth. + */ + uint8_t indent; + /** + * @var compact + * @brief Compact mode removes unnecessary spaces. + */ + bool compact; +}; + +/** + * @brief Escape and write the string \a s to the json file. + * + * @param[in] p Json print context. + * @param[in] s The string to write. + * + * @return 0 on success or a negative errno from fprintf(3). + */ +int xbps_json_print_escaped(struct xbps_json_printer *p, const char *s); + +/** + * @brief Write the string \a s as quoted string to the json file. + * + * @param[in] p Json print context. + * @param[in] s The string to write. + * + * @return 0 on success or a negative errno from fprintf(3). + */ +int xbps_json_print_quoted(struct xbps_json_printer *p, const char *s); + +/** + * @brief Write boolean to the json stream. + * + * @param[in] p Json print context. + * @param[in] b Boolean value. + * + * @return 0 on success or a negative errno from fprintf(3). + */ +int xbps_json_print_bool(struct xbps_json_printer *p, bool b); + +/** + * @brief Write a ::xbps_string_t to the json stream. + * + * @param[in] p Json print context. + * @param[in] str String value to print. + * + * @return 0 on success or a negative errno from fprintf(3). + */ +int xbps_json_print_xbps_string(struct xbps_json_printer *p, xbps_string_t str); + +/** + * @brief Write a ::xbps_number_t to the json stream. + * + * @param[in] p Json print context. + * @param[in] num Number value to print. + * + * @return 0 on success or a negative errno from fprintf(3). + */ +int xbps_json_print_xbps_number(struct xbps_json_printer *p, xbps_number_t num); + +/** + * @brief Write a ::xbps_boolean_t to the json stream. + * + * @param[in] p Json print context. + * @param[in] b Boolean value to print. + * + * @return 0 on success or a negative errno from fprintf(3). + */ +int xbps_json_print_xbps_boolean(struct xbps_json_printer *p, xbps_bool_t b); + +/** + * @brief Write a ::xbps_array_t to the json stream. + * + * @param[in] p Json print context. + * @param[in] array Array to print. + * + * @return 0 on success or a negative errno from fprintf(3). + */ +int xbps_json_print_xbps_array(struct xbps_json_printer *p, xbps_array_t array); + +/** + * @brief Write a ::xbps_dictionary_t to the json stream. + * + * @param[in] p Json print context. + * @param[in] dict Dictionary to print. + * + * @return 0 on success or a negative errno from fprintf(3). + */ +int xbps_json_print_xbps_dictionary(struct xbps_json_printer *p, xbps_dictionary_t dict); + +/** + * @brief Write a ::xbps_object_t to the json stream. + * + * @param[in] p Json print context. + * @param[in] obj Object to print. + * + * @return 0 on success or a negative errno from fprintf(3). + */ +int xbps_json_print_xbps_object(struct xbps_json_printer *p, xbps_object_t obj); + +/**@}*/ + +#endif /* !_XBPS_JSON_H_ */ diff --git a/include/xbps_api_impl.h b/include/xbps_api_impl.h index aac5b11b5..4282e4197 100644 --- a/include/xbps_api_impl.h +++ b/include/xbps_api_impl.h @@ -99,6 +99,7 @@ int HIDDEN xbps_transaction_fetch(struct xbps_handle *, int HIDDEN xbps_transaction_pkg_deps(struct xbps_handle *, xbps_array_t, xbps_dictionary_t); int HIDDEN xbps_transaction_internalize(struct xbps_handle *, xbps_object_iterator_t); +char HIDDEN *repo_format(struct xbps_handle *, const char *); char HIDDEN *xbps_get_remote_repo_string(const char *); int HIDDEN xbps_repo_sync(struct xbps_handle *, const char *); int HIDDEN xbps_file_hash_check_dictionary(struct xbps_handle *, diff --git a/lib/Makefile b/lib/Makefile index 0cf6ac84f..db3b2bdf9 100644 --- a/lib/Makefile +++ b/lib/Makefile @@ -51,7 +51,7 @@ OBJS += plist_remove.o plist_fetch.o util.o util_path.o util_hash.o OBJS += repo.o repo_sync.o OBJS += rpool.o cb_util.o proplib_wrapper.o OBJS += package_alternatives.o -OBJS += conf.o log.o +OBJS += conf.o log.o format.o json.o OBJS += $(EXTOBJS) $(COMPAT_OBJS) # unnecessary unless pkgdb format changes # OBJS += pkgdb_conversion.o diff --git a/lib/compat/humanize_number.c b/lib/compat/humanize_number.c index 446c94b83..c7c4bb336 100644 --- a/lib/compat/humanize_number.c +++ b/lib/compat/humanize_number.c @@ -1,7 +1,10 @@ /* $NetBSD: humanize_number.c,v 1.14 2008/04/28 20:22:59 martin Exp $ */ -/* +/*- + * SPDX-License-Identifier: BSD-2-Clause-FreeBSD + * * Copyright (c) 1997, 1998, 1999, 2002 The NetBSD Foundation, Inc. + * Copyright 2013 John-Mark Gurney * All rights reserved. * * This code is derived from software contributed to The NetBSD Foundation @@ -30,6 +33,7 @@ * POSSIBILITY OF SUCH DAMAGE. */ +#include #include #include #include @@ -37,61 +41,85 @@ #include #include -#include "xbps_api_impl.h" #include "compat.h" + +static const int maxscale = 6; + + int HIDDEN -humanize_number(char *buf, size_t len, int64_t bytes, - const char *suffix, int scale, int flags) +humanize_number(char *buf, size_t len, int64_t quotient, + const char *suffix, int scale, int flags) { const char *prefixes, *sep; - int b, i, r, maxscale, s1, s2, sign; + int i, r, remainder, s1, s2, sign; + int divisordeccut; int64_t divisor, max; size_t baselen; - assert(buf != NULL); - assert(suffix != NULL); - assert(scale >= 0); + /* Since so many callers don't check -1, NUL terminate the buffer */ + if (len > 0) + buf[0] = '\0'; - if (flags & HN_DIVISOR_1000) { - /* SI for decimal multiplies */ - divisor = 1000; - if (flags & HN_B) - prefixes = "B\0k\0M\0G\0T\0P\0E"; - else - prefixes = "\0\0k\0M\0G\0T\0P\0E"; - } else { + /* validate args */ + if (buf == NULL || suffix == NULL) + return (-1); + if (scale < 0) + return (-1); + else if (scale > maxscale && + ((scale & ~(HN_AUTOSCALE|HN_GETSCALE)) != 0)) + return (-1); + if ((flags & HN_DIVISOR_1000) && (flags & HN_IEC_PREFIXES)) + return (-1); + + /* setup parameters */ + remainder = 0; + + if (flags & HN_IEC_PREFIXES) { + baselen = 2; /* - * binary multiplies - * XXX IEC 60027-2 recommends Ki, Mi, Gi... + * Use the prefixes for power of two recommended by + * the International Electrotechnical Commission + * (IEC) in IEC 80000-3 (i.e. Ki, Mi, Gi...). + * + * HN_IEC_PREFIXES implies a divisor of 1024 here + * (use of HN_DIVISOR_1000 would have triggered + * an assertion earlier). */ divisor = 1024; + divisordeccut = 973; /* ceil(.95 * 1024) */ if (flags & HN_B) - prefixes = "B\0K\0M\0G\0T\0P\0E"; + prefixes = "B\0\0Ki\0Mi\0Gi\0Ti\0Pi\0Ei"; else - prefixes = "\0\0K\0M\0G\0T\0P\0E"; + prefixes = "\0\0\0Ki\0Mi\0Gi\0Ti\0Pi\0Ei"; + } else { + baselen = 1; + if (flags & HN_DIVISOR_1000) { + divisor = 1000; + divisordeccut = 950; + if (flags & HN_B) + prefixes = "B\0\0k\0\0M\0\0G\0\0T\0\0P\0\0E"; + else + prefixes = "\0\0\0k\0\0M\0\0G\0\0T\0\0P\0\0E"; + } else { + divisor = 1024; + divisordeccut = 973; /* ceil(.95 * 1024) */ + if (flags & HN_B) + prefixes = "B\0\0K\0\0M\0\0G\0\0T\0\0P\0\0E"; + else + prefixes = "\0\0\0K\0\0M\0\0G\0\0T\0\0P\0\0E"; + } } -#define SCALE2PREFIX(scale) (&prefixes[(scale) << 1]) - maxscale = 7; +#define SCALE2PREFIX(scale) (&prefixes[(scale) * 3]) - if (scale >= maxscale && - (scale & (HN_AUTOSCALE | HN_GETSCALE)) == 0) - return (-1); - - if (buf == NULL || suffix == NULL) - return (-1); - - if (len > 0) - buf[0] = '\0'; - if (bytes < 0) { + if (quotient < 0) { sign = -1; - bytes *= -100; - baselen = 3; /* sign, digit, prefix */ + quotient = -quotient; + baselen += 2; /* sign, digit */ } else { sign = 1; - bytes *= 100; - baselen = 2; /* digit, prefix */ + baselen += 1; /* digit */ } if (flags & HN_NOSPACE) sep = ""; @@ -107,7 +135,7 @@ humanize_number(char *buf, size_t len, int64_t bytes, if (scale & (HN_AUTOSCALE | HN_GETSCALE)) { /* See if there is additional columns can be used. */ - for (max = 100, i = (int)(len - baselen); i-- > 0;) + for (max = 1, i = len - baselen; i-- > 0;) max *= 10; /* @@ -115,29 +143,39 @@ humanize_number(char *buf, size_t len, int64_t bytes, * If there will be an overflow by the rounding below, * divide once more. */ - for (i = 0; bytes >= max - 50 && i < maxscale; i++) - bytes /= divisor; + for (i = 0; + (quotient >= max || (quotient == max - 1 && + (remainder >= divisordeccut || remainder >= + divisor / 2))) && i < maxscale; i++) { + remainder = quotient % divisor; + quotient /= divisor; + } if (scale & HN_GETSCALE) return (i); - } else - for (i = 0; i < scale && i < maxscale; i++) - bytes /= divisor; + } else { + for (i = 0; i < scale && i < maxscale; i++) { + remainder = quotient % divisor; + quotient /= divisor; + } + } /* If a value <= 9.9 after rounding and ... */ - if (bytes < 995 && i > 0 && flags & HN_DECIMAL) { - /* baselen + \0 + .N */ - if (len < baselen + 1 + 2) - return (-1); - b = ((int)bytes + 5) / 10; - s1 = b / 10; - s2 = b % 10; + /* + * XXX - should we make sure there is enough space for the decimal + * place and if not, don't do HN_DECIMAL? + */ + if (((quotient == 9 && remainder < divisordeccut) || quotient < 9) && + i > 0 && flags & HN_DECIMAL) { + s1 = (int)quotient + ((remainder * 10 + divisor / 2) / + divisor / 10); + s2 = ((remainder * 10 + divisor / 2) / divisor) % 10; r = snprintf(buf, len, "%d%s%d%s%s%s", sign * s1, localeconv()->decimal_point, s2, sep, SCALE2PREFIX(i), suffix); } else r = snprintf(buf, len, "%" PRId64 "%s%s%s", - sign * ((bytes + 50) / 100), + sign * (quotient + (remainder + divisor / 2) / divisor), sep, SCALE2PREFIX(i), suffix); return (r); diff --git a/lib/format.c b/lib/format.c new file mode 100644 index 000000000..a4362299c --- /dev/null +++ b/lib/format.c @@ -0,0 +1,820 @@ +/*- + * Copyright (c) 2023 Duncan Overbruck . + * All rights reserved. + * + * 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. + * + * THIS SOFTWARE IS PROVIDED BY THE AUTHOR ``AS IS'' AND ANY EXPRESS OR + * IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES + * OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE DISCLAIMED. + * IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR ANY DIRECT, INDIRECT, + * INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT + * NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, + * DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY + * THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT + * (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF + * THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. + */ + +#include +#include +#include +#include + +#include "compat.h" +#include "xbps/fmt.h" +#include "xbps/json.h" +#include "xbps/xbps_array.h" +#include "xbps/xbps_bool.h" +#include "xbps/xbps_data.h" +#include "xbps/xbps_dictionary.h" +#include "xbps/xbps_number.h" +#include "xbps/xbps_object.h" +#include "xbps/xbps_string.h" +#include "xbps_api_impl.h" + +/** + * @file lib/format.c + * @brief Format printing functions + * @defgroup format Format printing fuctions + * + * The format strings are similar to normal printf() format strings, + * but instead of character to specify types variable names are used. + * + */ + +struct xbps_fmt { + /** + * @brief Prefix of the format chunk. + */ + char *prefix; + /** + * @brief Variable in the format string. + */ + struct xbps_fmt_var var; +}; + +struct strbuf { + size_t sz, len; + char *mem; +}; + +static int +strbuf_grow(struct strbuf *sb, size_t n) +{ + char *tmp; + size_t nsz; + if (sb->len+n+1 < sb->sz) + return 0; + nsz = 2*sb->sz + 16; + tmp = realloc(sb->mem, nsz); + if (!tmp) + return -errno; + sb->mem = tmp; + sb->sz = nsz; + return 0; +} + +static int +strbuf_putc(struct strbuf *sb, char c) +{ + int r = strbuf_grow(sb, 1); + if (r < 0) + return 0; + sb->mem[sb->len++] = c; + sb->mem[sb->len] = '\0'; + return 0; +} + +static int +strbuf_puts(struct strbuf *sb, const char *s, size_t n) +{ + int r = strbuf_grow(sb, n); + if (r < 0) + return 0; + memcpy(sb->mem+sb->len, s, n); + sb->len += n; + sb->mem[sb->len] = '\0'; + return 0; +} + +static void +strbuf_reset(struct strbuf *sb) +{ + sb->len = 0; + if (sb->mem) + sb->mem[0] = '\0'; +} + +static void +strbuf_release(struct strbuf *sb) +{ + free(sb->mem); + sb->mem = NULL; + sb->len = sb->sz = 0; +} + +enum tok { + TTEXT = 1, + TVAR, +}; + +static int +nexttok(enum tok *tok, const char **pos, struct strbuf *buf) +{ + const char *p; + int r; + + strbuf_reset(buf); + + for (p = *pos; *p;) { + switch (*p) { + case '}': + return -EINVAL; + case '{': + *pos = p; + if (buf->len > 0) + *tok = TTEXT; + else + *tok = TVAR; + return 1; + case '\\': + switch (*++p) { + case '\\': r = strbuf_putc(buf, '\\'); break; + case 'a': r = strbuf_putc(buf, '\a'); break; + case 'b': r = strbuf_putc(buf, '\b'); break; + case 'e': r = strbuf_putc(buf, '\e'); break; + case 'f': r = strbuf_putc(buf, '\f'); break; + case 'n': r = strbuf_putc(buf, '\n'); break; + case 'r': r = strbuf_putc(buf, '\r'); break; + case 't': r = strbuf_putc(buf, '\t'); break; + case '0': r = strbuf_putc(buf, '\0'); break; + default: r = *p ? strbuf_putc(buf, *p) : 0; + } + if (r < 0) + return r; + if (*p) + p++; + break; + default: + r = strbuf_putc(buf, *p++); + if (r < 0) + return r; + } + } + if (buf->len > 0) { + *pos = p; + *tok = TTEXT; + return 1; + } + p++; + return 0; +} + +static int +parse_u(const char **pos, unsigned int *u) +{ + char *e = NULL; + long v; + errno = 0; + v = strtoul(*pos, &e, 10); + if (errno != 0) + return -errno; + if (v > UINT_MAX) + return -ERANGE; + *u = v; + *pos = e; + return 0; +} + +static int +parse_d(const char **pos, int64_t *d) +{ + char *e = NULL; + long v; + errno = 0; + v = strtol(*pos, &e, 10); + if (errno != 0) + return -errno; + if (v > UINT_MAX) + return -ERANGE; + *d = v; + *pos = e; + return 0; +} + +static int +parse_default(const char **pos, struct xbps_fmt_def **defp, struct strbuf *buf) +{ + struct strbuf buf2 = {0}; + struct xbps_fmt_def *def = *defp; + const char *p = *pos; + char *str = NULL; + int r; + + if (*p++ != '?') { + *defp = NULL; + return 0; + } + + if (!def) { + def = *defp = calloc(1, sizeof(*def)); + if (!def) + return -errno; + } + + if ((*p >= '0' && *p <= '9') || *p == '-') { + r = parse_d(&p, &def->val.num); + if (r < 0) + return r; + def->type = XBPS_FMT_DEF_NUM; + *pos = p; + return 0; + } else if (strncmp(p, "true", sizeof("true") - 1) == 0) { + *pos = p + sizeof("true") - 1; + def->type = XBPS_FMT_DEF_BOOL; + def->val.boolean = true; + return 0; + } else if (strncmp(p, "false", sizeof("false") - 1) == 0) { + *pos = p + sizeof("false") - 1; + def->type = XBPS_FMT_DEF_BOOL; + def->val.boolean = false; + return 0; + } + + if (*p++ != '"') + return -EINVAL; + + if (!buf) { + buf = &buf2; + } else { + r = strbuf_putc(buf, '\0'); + if (r < 0) + return r; + str = buf->mem + buf->len; + } + for (; *p && *p != '"';) { + if (*p == '\\') { + switch (*++p) { + case '\\': r = strbuf_putc(buf, '\\'); break; + case 'a': r = strbuf_putc(buf, '\a'); break; + case 'b': r = strbuf_putc(buf, '\b'); break; + case 'f': r = strbuf_putc(buf, '\f'); break; + case 'n': r = strbuf_putc(buf, '\n'); break; + case 'r': r = strbuf_putc(buf, '\r'); break; + case 't': r = strbuf_putc(buf, '\t'); break; + case '0': r = strbuf_putc(buf, '\0'); break; + default: r = *p ? strbuf_putc(buf, *p) : 0; + } + } else { + r = strbuf_putc(buf, *p); + } + if (r < 0) + goto err; + if (*p) + p++; + } + if (*p++ != '"') { + r = -EINVAL; + goto err; + } + *pos = p; + def->type = XBPS_FMT_DEF_STR; + if (buf == &buf2) { + def->val.str = strdup(buf2.mem ? buf2.mem : ""); + if (!def->val.str) { + r = -errno; + goto err; + } + strbuf_release(&buf2); + } else { + def->val.str = str; + } + return 0; +err: + strbuf_release(&buf2); + return r; +} + +struct xbps_fmt_conv { + enum { HUMANIZE = 1, STRMODE, JSON } type; + union { + struct humanize { + unsigned width : 8; + unsigned minscale : 8; + unsigned maxscale : 8; + bool decimal : 1; + int flags; + } humanize; + }; +}; + +static int +parse_humanize(const char **pos, struct humanize *humanize) +{ + const char *scale = "BKMGTPE"; + const char *p = *pos; + const char *p1; + + /* default: !humanize .8Ki:8 */ + humanize->width = 8; + humanize->minscale = 2; + humanize->flags = HN_DECIMAL|HN_IEC_PREFIXES; + humanize->flags = HN_NOSPACE; + + /* humanize[ ][.][i][width][minscale[maxscale]] */ + + if (*p == ' ') { + humanize->flags &= ~HN_NOSPACE; + p++; + } + if (*p == '.') { + humanize->flags |= HN_DECIMAL; + p++; + } + if ((*p >= '0' && *p <= '9')) { + unsigned width = 0; + int r = parse_u(&p, &width); + if (r < 0) + return r; + humanize->width = width <= 12 ? width : 12; + } + if ((p1 = strchr(scale, *p))) { + humanize->minscale = p1-scale+1; + p++; + if ((p1 = strchr(scale, *p))) { + humanize->maxscale = p1-scale+1; + p++; + } + } + if (*p == 'i') { + humanize->flags |= HN_IEC_PREFIXES; + p++; + } + *pos = p; + return 0; +} + +static int +parse_conversion(const char **pos, struct xbps_fmt_conv **convp) +{ + struct xbps_fmt_conv *conv = *convp; + + if (**pos != '!') { + *convp = NULL; + return 0; + } + + if (!conv) { + conv = *convp = calloc(1, sizeof(*conv)); + if (!conv) + return -errno; + } + if (strncmp(*pos + 1, "strmode", sizeof("strmode") - 1) == 0) { + *pos += sizeof("strmode"); + conv->type = STRMODE; + return 0; + } else if (strncmp(*pos + 1, "humanize", sizeof("humanize") - 1) == 0) { + conv->type = HUMANIZE; + *pos += sizeof("humanize"); + return parse_humanize(pos, &conv->humanize); + } else if (strncmp(*pos + 1, "json", sizeof("json") - 1) == 0) { + conv->type = JSON; + *pos += sizeof("json"); + return 0; + } + return -EINVAL; +} + +static int +parse_spec(const char **pos, struct xbps_fmt_spec **specp) +{ + bool fill = false; + struct xbps_fmt_spec *spec = *specp; + const char *p = *pos; + int r; + + /* format_spec ::= [[fill]align][sign][zero][width][.precision][type] */ + + if (*p != ':') { + *specp = NULL; + return 0; + } + p++; + + if (!spec) { + spec = *specp = calloc(1, sizeof(*spec)); + if (!spec) + return -errno; + } + + /* defaults */ + spec->fill = ' '; + spec->align = '>'; + spec->sign = '-'; + spec->width = 0; + spec->precision = 0; + spec->type = '\0'; + + /* fill ::= . */ + if (*p && strchr("<>=", p[1])) { + fill = true; + spec->fill = *p; + spec->align = p[1]; + p += 2; + } + + /* align ::= [<>=] */ + if (strchr("<>=", *p)) { + spec->align = *p; + p += 1; + } + + /* sign ::= [+-] */ + if (strchr("+- ", *p)) { + spec->sign = *p; + p += 1; + } + + /* zero ::= [0] */ + if (*p == '0') { + if (!fill) { + spec->fill = '0'; + spec->align = '='; + } + p++; + } + + /* width ::= [[0-9]+] */ + if ((*p >= '0' && *p <= '9')) { + r = parse_u(&p, &spec->width); + if (r < 0) + return r; + } + + /* precision ::= ['.' [0-9]+] */ + if (*p == '.') { + p++; + r = parse_u(&p, &spec->precision); + if (r < 0) + return r; + } + + /* type ::= [[a-zA-Z]] */ + if ((*p >= 'a' && *p <= 'z') || + (*p >= 'A' && *p <= 'Z')) + spec->type = *p++; + + *pos = p; + return 0; +} + +static int +parse(const char **pos, struct xbps_fmt *fmt, struct strbuf *buf) +{ + const char *p = *pos; + const char *e; + int r; + + if (*p != '{') + return -EINVAL; + p++; + + /* var ::= '{' name [default][conversion][format_spec] '}' */ + + /* name ::= [a-zA-Z0-9_-]+ */ + for (e = p; (*e >= 'a' && *e <= 'z') || + (*e >= 'A' && *e <= 'Z') || + (*e >= '0' && *e <= '0') || + (*e == '_' || *e == '-'); e++) + ; + if (e == p) + return -EINVAL; + + if (buf) { + strbuf_reset(buf); + r = strbuf_puts(buf, p, e - p); + if (r < 0) + return r; + fmt->var.name = buf->mem; + } else { + fmt->var.name = strndup(p, e - p); + if (!fmt->var.name) + return -errno; + } + p = e; + + /* default ::= ['?' ...] */ + r = parse_default(&p, &fmt->var.def, buf); + if (r < 0) + return r; + + /* conversion ::= ['!' ...] */ + r = parse_conversion(&p, &fmt->var.conv); + if (r < 0) + return r; + + /* format_spec ::= [':' ...] */ + r = parse_spec(&p, &fmt->var.spec); + if (r < 0) + return r; + + if (*p != '}') + return -EINVAL; + *pos = p+1; + return 0; +} + +struct xbps_fmt * +xbps_fmt_parse(const char *format) +{ + struct strbuf buf = {0}; + const char *pos = format; + struct xbps_fmt *fmt = NULL; + size_t n = 0; + int r = 1; + + for (;;) { + struct xbps_fmt *tmp; + enum tok t; + + r = nexttok(&t, &pos, &buf); + if (r < 0) + goto err; + + tmp = realloc(fmt, sizeof(*fmt)*(n + 1)); + if (!tmp) + goto err_errno; + fmt = tmp; + memset(&fmt[n], '\0', sizeof(struct xbps_fmt)); + + if (r == 0) + goto out; + if (t == TTEXT) { + fmt[n].prefix = strndup(buf.mem, buf.len); + if (!fmt[n].prefix) + goto err_errno; + r = nexttok(&t, &pos, &buf); + if (r < 0) + goto err; + } + if (t == TVAR) { + r = parse(&pos, &fmt[n], NULL); + if (r < 0) + goto err; + } + n++; + } +out: + strbuf_release(&buf); + return fmt; +err_errno: + r = -errno; +err: + free(fmt); + strbuf_release(&buf); + errno = -r; + return NULL; +} + +void +xbps_fmt_free(struct xbps_fmt *fmt) +{ + if (!fmt) + return; + for (struct xbps_fmt *f = fmt; f->prefix || f->var.name; f++) { + free(f->prefix); + free(f->var.name); + if (f->var.def && f->var.def->type == XBPS_FMT_DEF_STR) + free(f->var.def->val.str); + free(f->var.def); + free(f->var.spec); + free(f->var.conv); + } + free(fmt); +} + +int +xbps_fmts(const char *format, xbps_fmt_cb *cb, void *data, FILE *fp) +{ + struct strbuf buf = {0}; + const char *pos = format; + int r = 0; + + for (;;) { + enum tok t; + + r = nexttok(&t, &pos, &buf); + if (r <= 0) + goto out; + if (t == TTEXT) { + fprintf(fp, "%s", buf.mem); + r = nexttok(&t, &pos, &buf); + if (r <= 0) + goto out; + } + if (t == TVAR) { + struct xbps_fmt_def def = {0}; + struct xbps_fmt_conv conv = {0}; + struct xbps_fmt_spec spec = {0}; + struct xbps_fmt fmt = { + .var.name = buf.mem, + .var.conv = &conv, + .var.def = &def, + .var.spec = &spec, + }; + r = parse(&pos, &fmt, &buf); + if (r < 0) + goto out; + r = cb(fp, &fmt.var, data); + if (r != 0) + goto out; + } + } +out: + strbuf_release(&buf); + return r; +} + +int +xbps_fmt(const struct xbps_fmt *fmt, xbps_fmt_cb *cb, void *data, FILE *fp) +{ + int r; + for (const struct xbps_fmt *f = fmt; f->prefix || f->var.name; f++) { + if (f->prefix) + fprintf(fp, "%s", f->prefix); + if (f->var.name) { + r = cb(fp, &f->var, data); + if (r != 0) + return r; + } + } + return 0; +} + +int +xbps_fmt_print_string(const struct xbps_fmt_var *var, const char *str, size_t len, FILE *fp) +{ + const struct xbps_fmt_spec *spec = var->spec; + + if (var->conv && var->conv->type == JSON) { + struct xbps_json_printer pr = {.file = fp}; + return xbps_json_print_quoted(&pr, str); + } + + if (len == 0) + len = strlen(str); + if (spec && spec->align == '>' && spec->width > (unsigned)len) { + for (unsigned i = 0; i < spec->width - len; i++) + fputc(spec->fill, fp); + } + fprintf(fp, "%.*s", (int)len, str); + if (spec && spec->align == '<' && spec->width > (unsigned)len) { + for (unsigned i = 0; i < spec->width - len; i++) + fputc(spec->fill, fp); + } + return 0; +} + +static int +humanize(const struct xbps_fmt_var *var, int64_t d, FILE *fp) +{ + char buf[64]; + struct humanize *h = &var->conv->humanize; + int scale = 0; + int width = h->width ? h->width : 8; + int len; + + if (h->minscale) { + scale = humanize_number(buf, width, d, "B", HN_GETSCALE, h->flags); + if (scale == -1) + return -EINVAL; + if (scale < h->minscale - 1) + scale = h->minscale - 1; + if (h->maxscale && scale > h->maxscale - 1) + scale = h->maxscale - 1; + } else if (scale == 0) { + scale = HN_AUTOSCALE; + } + len = humanize_number(buf, width, d, "B", scale, h->flags); + if (len == -1) + return -EINVAL; + return xbps_fmt_print_string(var, buf, len, fp); +} + +static int +tostrmode(const struct xbps_fmt_var *var UNUSED, int64_t d UNUSED, FILE *fp UNUSED) +{ + return -ENOTSUP; +} + +int +xbps_fmt_print_number(const struct xbps_fmt_var *var, int64_t d, FILE *fp) +{ + char buf[64]; + struct xbps_fmt_spec strspec = {0}; + struct xbps_fmt_var strfmt = { .spec = &strspec }; + struct xbps_fmt_spec *spec = var->spec; + const char *p = buf; + int len; + + if (var->conv) { + switch (var->conv->type) { + case HUMANIZE: return humanize(var, d, fp); + case STRMODE: return tostrmode(var, d, fp); + case JSON: break; + } + } + if (spec) { + strspec = *spec; + if (spec->align == '=') + strspec.align = '>'; + } + + switch (spec ? spec->type : '\0') { + default: /* fallthrough */ + case 'd': + if (spec && spec->sign == '+') + len = snprintf(buf, sizeof(buf), "%+" PRId64, d); + else + len = snprintf(buf, sizeof(buf), "%" PRId64, d); + if (spec && spec->align == '=' && (buf[0] == '+' || buf[0] == '-')) { + len--, p++; + strspec.width -= 1; + fputc(buf[0], fp); + } + break; + case 'o': len = snprintf(buf, sizeof(buf), "%" PRIo64, d); break; + case 'u': len = snprintf(buf, sizeof(buf), "%" PRIu64, d); break; + case 'x': len = snprintf(buf, sizeof(buf), "%" PRIx64, d); break; + case 'X': len = snprintf(buf, sizeof(buf), "%" PRIX64, d); break; + } + return xbps_fmt_print_string(&strfmt, p, len, fp); +} + +int +xbps_fmt_print_object(const struct xbps_fmt_var *var, xbps_object_t obj, FILE *fp) +{ + if (var->conv && var->conv->type == JSON) { + struct xbps_json_printer pr = {.file = fp}; + return xbps_json_print_xbps_object(&pr, obj); + } + switch (xbps_object_type(obj)) { + case XBPS_TYPE_BOOL: + return xbps_fmt_print_string(var, xbps_bool_true(obj) ? "true" : "false", 0, fp); + case XBPS_TYPE_NUMBER: + return xbps_fmt_print_number(var, xbps_number_integer_value(obj), fp); + case XBPS_TYPE_STRING: + return xbps_fmt_print_string(var, xbps_string_cstring_nocopy(obj), + xbps_string_size(obj), fp); + case XBPS_TYPE_UNKNOWN: + if (var->def) { + struct xbps_fmt_def *def = var->def; + switch (var->def->type) { + case XBPS_FMT_DEF_BOOL: + return xbps_fmt_print_string(var, def->val.boolean ? + "true" : "false", 0, fp); + case XBPS_FMT_DEF_STR: + return xbps_fmt_print_string(var, def->val.str, 0, fp); + case XBPS_FMT_DEF_NUM: + return xbps_fmt_print_number(var, def->val.num, fp); + } + } + default: + break; + } + return 0; +} + +struct fmt_dict_ctx { + xbps_dictionary_t dict; +}; + +static int +fmt_dict_cb(FILE *fp, const struct xbps_fmt_var *var, void *data) +{ + struct fmt_dict_ctx *ctx = data; + xbps_object_t obj = xbps_dictionary_get(ctx->dict, var->name); + return xbps_fmt_print_object(var, obj, fp); +} + +int +xbps_fmt_dictionary(const struct xbps_fmt *fmt, xbps_dictionary_t dict, FILE *fp) +{ + struct fmt_dict_ctx ctx = {.dict = dict}; + return xbps_fmt(fmt, &fmt_dict_cb, &ctx, fp); +} + +int +xbps_fmts_dictionary(const char *format, xbps_dictionary_t dict, FILE *fp) +{ + struct fmt_dict_ctx ctx = {.dict = dict}; + return xbps_fmts(format, &fmt_dict_cb, &ctx, fp); +} diff --git a/lib/initend.c b/lib/initend.c index 547db6a98..f3d88b9a8 100644 --- a/lib/initend.c +++ b/lib/initend.c @@ -164,6 +164,15 @@ xbps_init(struct xbps_handle *xhp) xhp->flags |= XBPS_FLAG_DISABLE_SYSLOG; } + for (unsigned int i = 0; i < xbps_array_count(xhp->repositories); i++) { + const char *url = NULL; + xbps_array_get_cstring_nocopy(xhp->repositories, i, &url); + xbps_array_set_cstring_nocopy(xhp->repositories, i, repo_format(xhp, url)); + } + + xbps_dbg_printf("Native architecture is %s\n", xhp->native_arch); + xbps_dbg_printf("Target architecture is %s\n", xhp->target_arch); + if (xhp->flags & XBPS_FLAG_DEBUG) { const char *repodir; for (unsigned int i = 0; i < xbps_array_count(xhp->repositories); i++) { diff --git a/lib/json.c b/lib/json.c new file mode 100644 index 000000000..ada02018b --- /dev/null +++ b/lib/json.c @@ -0,0 +1,202 @@ +/* SPDX-FileCopyrightText: Copyright 2023 Duncan Overbruck */ +/* SPDX-License-Identifier: BSD-2-Clause */ + +#include + +#include +#include +#include +#include +#include +#include + +#include "xbps/json.h" +#include "xbps/xbps_array.h" +#include "xbps/xbps_bool.h" +#include "xbps/xbps_dictionary.h" +#include "xbps/xbps_number.h" +#include "xbps/xbps_object.h" +#include "xbps/xbps_string.h" + +static int __attribute__ ((format (printf, 2, 3))) +xbps_json_printf(struct xbps_json_printer *p, const char *fmt, ...) +{ + va_list ap; + int r = 0; + va_start(ap, fmt); + if (vfprintf(p->file, fmt, ap) < 0) + r = errno ? -errno : -EIO; + va_end(ap); + return r; +} + +int +xbps_json_print_escaped(struct xbps_json_printer *p, const char *s) +{ + int r = 0; + for (; r >= 0 && *s; s++) { + switch (*s) { + case '"': r = xbps_json_printf(p, "\\\""); break; + case '\\': r = xbps_json_printf(p, "\\\\"); break; + case '\b': r = xbps_json_printf(p, "\\b"); break; + case '\f': r = xbps_json_printf(p, "\\f"); break; + case '\n': r = xbps_json_printf(p, "\\n"); break; + case '\r': r = xbps_json_printf(p, "\\r"); break; + case '\t': r = xbps_json_printf(p, "\\t"); break; + default: + if ((unsigned)*s < 0x20) { + r = xbps_json_printf(p, "\\u%04x", *s); + } else { + r = xbps_json_printf(p, "%c", *s); + } + } + } + return r; +} + +int +xbps_json_print_quoted(struct xbps_json_printer *p, const char *s) +{ + int r; + if ((r = xbps_json_printf(p, "\"")) < 0) + return r; + if ((r = xbps_json_print_escaped(p, s)) < 0) + return r; + return xbps_json_printf(p, "\""); +} + +int +xbps_json_print_bool(struct xbps_json_printer *p, bool b) +{ + return xbps_json_printf(p, b ? "true" : "false"); +} + +int +xbps_json_print_xbps_string(struct xbps_json_printer *p, xbps_string_t str) +{ + return xbps_json_print_quoted(p, xbps_string_cstring_nocopy(str)); +} + +int +xbps_json_print_xbps_number(struct xbps_json_printer *p, xbps_number_t num) +{ + if (xbps_number_unsigned(num)) { + return xbps_json_printf(p, "%" PRIu64, xbps_number_unsigned_integer_value(num)); + } else { + return xbps_json_printf(p, "%" PRId64, xbps_number_integer_value(num)); + } + return 0; +} + +int +xbps_json_print_xbps_boolean(struct xbps_json_printer *p, xbps_bool_t b) +{ + return xbps_json_print_bool(p, xbps_bool_true(b)); +} + +int +xbps_json_print_xbps_array(struct xbps_json_printer *p, xbps_array_t array) +{ + const char *item_sep = p->compact ? "," : ", "; + int indent = 0; + unsigned i = 0; + int r; + p->depth++; + if (!p->compact && p->indent > 0) { + indent = p->indent*p->depth; + item_sep = ",\n"; + } + if ((r = xbps_json_printf(p, "[")) < 0) + return r; + for (; i < xbps_array_count(array); i++) { + if (i == 0) { + if (indent > 0 && (r = xbps_json_printf(p, "\n%*s", indent, "")) < 0) + return r; + } else if ((r = xbps_json_printf(p, "%s%*s", item_sep, indent, "")) < 0) { + return r; + } + if ((r = xbps_json_print_xbps_object(p, xbps_array_get(array, i))) < 0) + return r; + } + + p->depth--; + if (indent > 0 && i > 0) + return xbps_json_printf(p, "\n%*s]", p->indent*p->depth, ""); + return xbps_json_printf(p, "]"); +} + +int +xbps_json_print_xbps_dictionary(struct xbps_json_printer *p, xbps_dictionary_t dict) +{ + xbps_object_iterator_t iter; + xbps_object_t keysym; + const char *item_sep = p->compact ? "," : ", "; + const char *key_sep = p->compact ? ":": ": "; + bool first = true; + int indent = 0; + int r; + + iter = xbps_dictionary_iterator(dict); + if (!iter) + return errno ? -errno : -ENOMEM; + + p->depth++; + if (!p->compact && p->indent > 0) { + indent = p->depth*p->indent; + item_sep = ",\n"; + } + + if ((r = xbps_json_printf(p, "{")) < 0) + goto err; + + while ((keysym = xbps_object_iterator_next(iter))) { + xbps_object_t obj; + const char *key; + + if (first) { + first = false; + if (p->indent > 0 && (r = xbps_json_printf(p, "\n%*s", indent, "")) < 0) { + goto err; + } + } else if ((r = xbps_json_printf(p, "%s%*s", item_sep, indent, "")) < 0) { + goto err; + } + + key = xbps_dictionary_keysym_cstring_nocopy(keysym); + if ((r = xbps_json_print_quoted(p, key)) < 0) + goto err; + if ((r = xbps_json_printf(p, "%s", key_sep)) < 0) + goto err; + + obj = xbps_dictionary_get_keysym(dict, keysym); + if ((r = xbps_json_print_xbps_object(p, obj)) < 0) + goto err; + } + + xbps_object_iterator_release(iter); + p->depth--; + if (indent > 0 && !first) + return xbps_json_printf(p, "\n%*s}", p->indent*p->depth, ""); + return xbps_json_printf(p, "}"); + +err: + xbps_object_iterator_release(iter); + return r; +} + +int +xbps_json_print_xbps_object(struct xbps_json_printer *p, xbps_object_t obj) +{ + if (!obj) return xbps_json_printf(p, "null"); + switch (xbps_object_type(obj)) { + case XBPS_TYPE_ARRAY: return xbps_json_print_xbps_array(p, obj); + case XBPS_TYPE_BOOL: return xbps_json_print_xbps_boolean(p, obj); + case XBPS_TYPE_DATA: return xbps_json_printf(p, "true"); + case XBPS_TYPE_DICTIONARY: return xbps_json_print_xbps_dictionary(p, obj); + case XBPS_TYPE_DICT_KEYSYM: return -EINVAL; + case XBPS_TYPE_NUMBER: return xbps_json_print_xbps_number(p, obj); + case XBPS_TYPE_STRING: return xbps_json_print_xbps_string(p, obj); + case XBPS_TYPE_UNKNOWN: return -EINVAL; + } + return -EINVAL; +} diff --git a/lib/repo.c b/lib/repo.c index 8d2aa6bd1..611e09008 100644 --- a/lib/repo.c +++ b/lib/repo.c @@ -40,6 +40,7 @@ #include #include +#include "xbps/fmt.h" #include "xbps_api_impl.h" /** @@ -196,6 +197,65 @@ repo_open_remote(struct xbps_repo *repo) return rv; } +static int +repo_fmt(FILE *fp, const struct xbps_fmt_var *var, void *data) +{ + struct xbps_handle *xhp = data; + const char *arch; + + if (xhp->target_arch) + arch = xhp->target_arch; + else + arch = xhp->native_arch; + + if (strcmp(var->name, "arch") == 0) { + return xbps_fmt_print_string(var, arch, 0, fp); + } + return 0; +} + +char * +repo_format(struct xbps_handle *xhp, const char *url) +{ + struct xbps_fmt *fmt = NULL; + FILE *fmt_stream; + char *fmt_buf; + size_t len; + int r; + + assert(xhp); + assert(url); + + if (!strstr(url, "{arch}")) + return strdup(url); + + xbps_dbg_printf("Processing templated repository: %s\n", url); + + fmt_stream = open_memstream(&fmt_buf, &len); + if (!fmt_stream) { + xbps_error_printf("failed to open buffer: %s\n", strerror(errno)); + goto fmtout; + } + fmt = xbps_fmt_parse(url); + if (!fmt) { + xbps_error_printf("failed to parse format for repo '%s': %s\n", url, strerror(errno)); + goto fmtout; + } + r = xbps_fmt(fmt, repo_fmt, xhp, fmt_stream); + if (r < 0) { + xbps_error_printf("failed to format repo '%s': %s\n", url, strerror(-r)); + goto fmtout; + } + fflush(fmt_stream); + return fmt_buf; + +fmtout: + fclose(fmt_stream); + xbps_fmt_free(fmt); + free(fmt_buf); + return NULL; +} + static struct xbps_repo * repo_open_with_type(struct xbps_handle *xhp, const char *url, const char *name) { diff --git a/tests/xbps/libxbps/Kyuafile b/tests/xbps/libxbps/Kyuafile index 86a009d9e..e0fa03a83 100644 --- a/tests/xbps/libxbps/Kyuafile +++ b/tests/xbps/libxbps/Kyuafile @@ -12,3 +12,5 @@ include('config/Kyuafile') include('find_pkg_orphans/Kyuafile') include('pkgdb/Kyuafile') include('shell/Kyuafile') +include('fmt/Kyuafile') +include('json/Kyuafile') diff --git a/tests/xbps/libxbps/Makefile b/tests/xbps/libxbps/Makefile index ca361bc65..ca884a459 100644 --- a/tests/xbps/libxbps/Makefile +++ b/tests/xbps/libxbps/Makefile @@ -12,5 +12,7 @@ SUBDIRS += find_pkg_orphans SUBDIRS += pkgdb SUBDIRS += config SUBDIRS += shell +SUBDIRS += fmt +SUBDIRS += json include ../../../mk/subdir.mk diff --git a/tests/xbps/libxbps/fmt/Kyuafile b/tests/xbps/libxbps/fmt/Kyuafile new file mode 100644 index 000000000..1a426bbdb --- /dev/null +++ b/tests/xbps/libxbps/fmt/Kyuafile @@ -0,0 +1,5 @@ +syntax("kyuafile", 1) + +test_suite("libxbps") + +atf_test_program{name="fmt_test"} diff --git a/tests/xbps/libxbps/fmt/Makefile b/tests/xbps/libxbps/fmt/Makefile new file mode 100644 index 000000000..c7749f64b --- /dev/null +++ b/tests/xbps/libxbps/fmt/Makefile @@ -0,0 +1,8 @@ +TOPDIR = ../../../.. +-include $(TOPDIR)/config.mk + +TESTSSUBDIR = xbps/libxbps/fmt +TEST = fmt_test +EXTRA_FILES = Kyuafile + +include $(TOPDIR)/mk/test.mk diff --git a/tests/xbps/libxbps/fmt/main.c b/tests/xbps/libxbps/fmt/main.c new file mode 100644 index 000000000..ac9ddfb84 --- /dev/null +++ b/tests/xbps/libxbps/fmt/main.c @@ -0,0 +1,183 @@ +/*- + * Copyright (c) 2023 Duncan Overbruck . + * All rights reserved. + * + * 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. + * + * THIS SOFTWARE IS PROVIDED BY THE AUTHOR ``AS IS'' AND ANY EXPRESS OR + * IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES + * OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE DISCLAIMED. + * IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR ANY DIRECT, INDIRECT, + * INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT + * NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, + * DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY + * THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT + * (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF + * THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. + *- + */ +#include "xbps.h" +#include "xbps/xbps_dictionary.h" +#include "xbps/xbps_object.h" +#include +#include +#include +#include +#include + +#include +#include +#include + +ATF_TC(xbps_fmt_print_number); + +ATF_TC_HEAD(xbps_fmt_print_number, tc) +{ + atf_tc_set_md_var(tc, "descr", "Test xbps_fmt_print_number"); +} + +ATF_TC_BODY(xbps_fmt_print_number, tc) +{ + char *buf = NULL; + size_t bufsz = 0; + FILE *fp; + struct test { + const char *expect; + int64_t d; + struct xbps_fmt_spec spec; + } tests[] = { + { "1", 1, {0}}, + {"-1", -1, {0}}, + {"-1", -1, {.sign = '+'}}, + {"+1", 1, {.sign = '+'}}, + + {"a", 0xA, {.type = 'x'}}, + {"A", 0xA, {.type = 'X'}}, + + {"644", 0644, {.type = 'o'}}, + + {"0010", 10, {.fill = '0', .align = '>', .width = 4}}, + {"1000", 10, {.fill = '0', .align = '<', .width = 4}}, + {"0010", 10, {.fill = '0', .align = '=', .width = 4}}, + {"-010", -10, {.fill = '0', .align = '=', .width = 4}}, + {"+010", 10, {.fill = '0', .align = '=', .sign = '+', .width = 4}}, + }; + ATF_REQUIRE(fp = open_memstream(&buf, &bufsz)); + + for (unsigned i = 0; i < sizeof(tests)/sizeof(tests[0]); i++) { + struct xbps_fmt_var var = { .spec = &tests[i].spec }; + memset(buf, '\0', bufsz); + rewind(fp); + xbps_fmt_print_number(&var, tests[i].d, fp); + ATF_REQUIRE(fflush(fp) == 0); + ATF_CHECK_STREQ(buf, tests[i].expect); + } + ATF_REQUIRE(fclose(fp) == 0); + free(buf); +} + +ATF_TC(xbps_fmt_print_string); + +ATF_TC_HEAD(xbps_fmt_print_string, tc) +{ + atf_tc_set_md_var(tc, "descr", "Test xbps_fmt_print_string"); +} + +ATF_TC_BODY(xbps_fmt_print_string, tc) +{ + char *buf = NULL; + size_t bufsz = 0; + FILE *fp; + struct test { + const char *expect; + const char *input; + size_t len; + struct xbps_fmt_spec spec; + } tests[] = { + { "1", "1", 0, {0}}, + { "2 ", "2", 0, {.fill = ' ', .align = '<', .width = 2}}, + { " 3", "3", 0, {.fill = ' ', .align = '>', .width = 2}}, + { "444", "444", 0, {.fill = ' ', .align = '>', .width = 2}}, + { "44", "444", 2, {.fill = ' ', .align = '>', .width = 2}}, + }; + ATF_REQUIRE(fp = open_memstream(&buf, &bufsz)); + + for (unsigned i = 0; i < sizeof(tests)/sizeof(tests[0]); i++) { + struct xbps_fmt_var var = { .spec = &tests[i].spec }; + memset(buf, '\0', bufsz); + rewind(fp); + xbps_fmt_print_string(&var, tests[i].input, tests[i].len, fp); + ATF_REQUIRE(fflush(fp) == 0); + ATF_CHECK_STREQ(buf, tests[i].expect); + } + ATF_REQUIRE(fclose(fp) == 0); + free(buf); +} + +ATF_TC(xbps_fmt_dictionary); + +ATF_TC_HEAD(xbps_fmt_dictionary, tc) +{ + atf_tc_set_md_var(tc, "descr", "Test xbps_fmt_dictionary"); +} + +ATF_TC_BODY(xbps_fmt_dictionary, tc) +{ + char *buf = NULL; + size_t bufsz = 0; + FILE *fp; + struct xbps_fmt *fmt; + xbps_dictionary_t dict; + ATF_REQUIRE(fp = open_memstream(&buf, &bufsz)); + ATF_REQUIRE(dict = xbps_dictionary_create()); + ATF_REQUIRE(xbps_dictionary_set_cstring_nocopy(dict, "string", "s")); + ATF_REQUIRE(xbps_dictionary_set_int64(dict, "number", 1)); + ATF_REQUIRE(fmt = xbps_fmt_parse(">{string} {number} {number!humanize} {foo?\"bar\"} {n?1000!humanize}<")); + ATF_REQUIRE(xbps_fmt_dictionary(fmt, dict, fp) == 0); + ATF_REQUIRE(fflush(fp) == 0); + ATF_CHECK_STREQ(buf, ">s 1 0KB bar 1KB<"); + ATF_REQUIRE(fclose(fp) == 0); + free(buf); + xbps_object_release(dict); +} + +ATF_TC(xbps_fmts_dictionary); + +ATF_TC_HEAD(xbps_fmts_dictionary, tc) +{ + atf_tc_set_md_var(tc, "descr", "Test xbps_fmt_dictionary"); +} + +ATF_TC_BODY(xbps_fmts_dictionary, tc) +{ + char *buf = NULL; + size_t bufsz = 0; + FILE *fp; + xbps_dictionary_t dict; + ATF_REQUIRE(fp = open_memstream(&buf, &bufsz)); + ATF_REQUIRE(dict = xbps_dictionary_create()); + ATF_REQUIRE(xbps_dictionary_set_cstring_nocopy(dict, "string", "s")); + ATF_REQUIRE(xbps_dictionary_set_int64(dict, "number", 1)); + ATF_REQUIRE(xbps_fmts_dictionary(">{string} {number} {number!humanize}<", dict, fp) == 0); + ATF_REQUIRE(fflush(fp) == 0); + ATF_CHECK_STREQ(buf, ">s 1 0KB<"); + ATF_REQUIRE(fclose(fp) == 0); + free(buf); + xbps_object_release(dict); +} + +ATF_TP_ADD_TCS(tp) +{ + ATF_TP_ADD_TC(tp, xbps_fmt_print_number); + ATF_TP_ADD_TC(tp, xbps_fmt_print_string); + ATF_TP_ADD_TC(tp, xbps_fmt_dictionary); + ATF_TP_ADD_TC(tp, xbps_fmts_dictionary); + return atf_no_error(); +} diff --git a/tests/xbps/libxbps/json/Kyuafile b/tests/xbps/libxbps/json/Kyuafile new file mode 100644 index 000000000..ae5491bb4 --- /dev/null +++ b/tests/xbps/libxbps/json/Kyuafile @@ -0,0 +1,5 @@ +syntax("kyuafile", 1) + +test_suite("libxbps") + +atf_test_program{name="json_test"} diff --git a/tests/xbps/libxbps/json/Makefile b/tests/xbps/libxbps/json/Makefile new file mode 100644 index 000000000..ea9786818 --- /dev/null +++ b/tests/xbps/libxbps/json/Makefile @@ -0,0 +1,8 @@ +TOPDIR = ../../../.. +-include $(TOPDIR)/config.mk + +TESTSSUBDIR = xbps/libxbps/json +TEST = json_test +EXTRA_FILES = Kyuafile + +include $(TOPDIR)/mk/test.mk diff --git a/tests/xbps/libxbps/json/main.c b/tests/xbps/libxbps/json/main.c new file mode 100644 index 000000000..5239cbc37 --- /dev/null +++ b/tests/xbps/libxbps/json/main.c @@ -0,0 +1,155 @@ +/* SPDX-FileCopyrightText: Copyright 2023 Duncan Overbruck */ +/* SPDX-License-Identifier: BSD-2-Clause */ + +#include +#include +#include +#include + +#include + +#include +#include +#include + +ATF_TC(xbps_json_print_escape); + +ATF_TC_HEAD(xbps_json_print_escape, tc) +{ + atf_tc_set_md_var(tc, "descr", "Test xbps_json_print_escape"); +} + +ATF_TC_BODY(xbps_json_print_escape, tc) +{ + char *buf = NULL; + size_t bufsz = 0; + struct xbps_json_printer p = {0}; + + ATF_REQUIRE(p.file = open_memstream(&buf, &bufsz)); + + ATF_CHECK_EQ(0, xbps_json_print_escaped(&p, "\"\\\b\f\n\r\t")); + ATF_REQUIRE_EQ(0, fflush(p.file)); + ATF_CHECK_STREQ("\\\"\\\\\\b\\f\\n\\r\\t", buf); + + memset(buf, '\0', bufsz); + ATF_CHECK_EQ(0, fseek(p.file, 0, SEEK_SET)); + ATF_CHECK_EQ(0, xbps_json_print_escaped(&p, "09azAZ !$#%^()%")); + ATF_REQUIRE_EQ(0, fflush(p.file)); + ATF_CHECK_STREQ("09azAZ !$#%^()%", buf); + + memset(buf, '\0', bufsz); + ATF_CHECK_EQ(0, fseek(p.file, 0, SEEK_SET)); + ATF_CHECK_EQ(0, xbps_json_print_escaped(&p, "\x01\x1F")); + ATF_REQUIRE_EQ(0, fflush(p.file)); + ATF_CHECK_STREQ("\\u0001\\u001f", buf); +} + +ATF_TC(xbps_json_print_xbps_dictionary); + +ATF_TC_HEAD(xbps_json_print_xbps_dictionary, tc) +{ + atf_tc_set_md_var(tc, "descr", "Test xbps_json_print_escape"); +} + +ATF_TC_BODY(xbps_json_print_xbps_dictionary, tc) +{ + char *buf = NULL; + size_t bufsz = 0; + struct xbps_json_printer p = {0}; + xbps_dictionary_t dict; + static const char *s = "" + "\n" + "\n" + "\n" + "array-empty\n" + "\n" + "\n" + "array-numbers\n" + "\n" + " 1\n" + " 2\n" + " 3\n" + "\n" + "dict-empty\n" + "\n" + "num-signed\n" + "1\n" + "num-unsigned\n" + "0x1\n" + "string\n" + "hello world\n" + "\n" + "\n"; + + ATF_REQUIRE(dict = xbps_dictionary_internalize(s)); + ATF_REQUIRE(p.file = open_memstream(&buf, &bufsz)); + + ATF_REQUIRE_EQ(0, xbps_json_print_xbps_dictionary(&p, dict)); + ATF_REQUIRE_EQ(0, fflush(p.file)); + ATF_CHECK_STREQ("{\"array-empty\": [], \"array-numbers\": [1, 2, 3], \"dict-empty\": {}, \"num-signed\": 1, \"num-unsigned\": 1, \"string\": \"hello world\"}", buf); +} + +ATF_TC(xbps_json_print_xbps_dictionary_indented); + +ATF_TC_HEAD(xbps_json_print_xbps_dictionary_indented, tc) +{ + atf_tc_set_md_var(tc, "descr", "Test xbps_json_print_xbps_dictionary: with indents"); +} + +ATF_TC_BODY(xbps_json_print_xbps_dictionary_indented, tc) +{ + char *buf = NULL; + size_t bufsz = 0; + struct xbps_json_printer p = {.indent = 2}; + xbps_dictionary_t dict; + static const char *s = "" + "\n" + "\n" + "\n" + "array-empty\n" + "\n" + "\n" + "array-numbers\n" + "\n" + " 1\n" + " 2\n" + " 3\n" + "\n" + "dict-empty\n" + "\n" + "num-signed\n" + "1\n" + "num-unsigned\n" + "0x1\n" + "string\n" + "hello world\n" + "\n" + "\n"; + + ATF_REQUIRE(dict = xbps_dictionary_internalize(s)); + ATF_REQUIRE(p.file = open_memstream(&buf, &bufsz)); + + ATF_REQUIRE_EQ(0, xbps_json_print_xbps_dictionary(&p, dict)); + ATF_REQUIRE_EQ(0, fflush(p.file)); + ATF_CHECK_STREQ( + "{\n" + " \"array-empty\": [],\n" + " \"array-numbers\": [\n" + " 1,\n" + " 2,\n" + " 3\n" + " ],\n" + " \"dict-empty\": {},\n" + " \"num-signed\": 1,\n" + " \"num-unsigned\": 1,\n" + " \"string\": \"hello world\"\n" + "}", buf); +} + +ATF_TP_ADD_TCS(tp) +{ + ATF_TP_ADD_TC(tp, xbps_json_print_escape); + ATF_TP_ADD_TC(tp, xbps_json_print_xbps_dictionary); + ATF_TP_ADD_TC(tp, xbps_json_print_xbps_dictionary_indented); + return atf_no_error(); +}