diff --git a/docs/man/rpm.8.md b/docs/man/rpm.8.md index 8ce06cf3ee..43ecab3b49 100644 --- a/docs/man/rpm.8.md +++ b/docs/man/rpm.8.md @@ -66,7 +66,7 @@ query-options General: \[**\--changelog**\] \[**\--changes**\] \[**\--dupes**\] \[**-i,\--info**\] \[**\--last**\] \[**\--qf,\--queryformat -***QUERYFMT*\] \[**\--xml**\] +***QUERYFMT*\] \[**\--xml**\] \[**\--json**\] Dependencies: \[**\--conflicts**\] \[**\--enhances**\] \[**\--obsoletes**\] \[**\--provides**\] \[**\--recommends**\] @@ -552,6 +552,10 @@ Alternate output formats may be requested by following the tag with : Human readable number (in SI). The suffix K = 1000, M = 1000000, \... +**:json** + +: Wrap data in JSON. + **:perms** : Format file permissions. diff --git a/include/rpm/rpmtd.h b/include/rpm/rpmtd.h index c592905fad..d3c934edf4 100644 --- a/include/rpm/rpmtd.h +++ b/include/rpm/rpmtd.h @@ -253,6 +253,7 @@ typedef enum rpmtdFormats_e { RPMTD_FORMAT_HUMANIEC = 21, /* human readable value, K = 1024 (int types) */ RPMTD_FORMAT_TAGNAME = 22, /* tag name (any type) */ RPMTD_FORMAT_TAGNUM = 23, /* tag number (any type) */ + RPMTD_FORMAT_JSON = 24, /* json format (any type) */ } rpmtdFormats; /** \ingroup rpmtd diff --git a/lib/formats.c b/lib/formats.c index 156812165f..3e001c604e 100644 --- a/lib/formats.c +++ b/lib/formats.c @@ -339,6 +339,81 @@ static char * xmlFormat(rpmtd td, char **emsg) return val; } +static char *jsonEscape(const char *s) +{ + char *es = NULL; + rstrcat(&es, "\""); + for (const char *c = s; *c != '\0'; c++) { + const char *ec = NULL; + switch (*c) { + case '\b': + ec = "\\b"; + break; + case '\f': + ec = "\\f"; + break; + case '\n': + ec = "\\n"; + break; + case '\r': + ec = "\\r"; + break; + case '\t': + ec = "\\t"; + break; + case '"': + ec = "\\\""; + break; + case '\\': + ec = "\\\\"; + break; + default: + break; + } + + if (ec) { + rstrcat(&es, ec); + } else if (*c > 0 && *c < 0x20) { + char *uc = NULL; + rasprintf(&uc, "\\u%04x", *c); + rstrcat(&es, uc); + free(uc); + } else { + char t[2] = " "; + *t = *c; + rstrcat(&es, t); + } + } + rstrcat(&es, "\""); + return es; +} + +static char *jsonFormat(rpmtd td, char **emsg) +{ + int escape = 1; + char *val = NULL; + + switch (rpmtdClass(td)) { + case RPM_BINARY_CLASS: + /* we don't want newlines in the binary output */ + val = rpmBase64Encode(td->data, td->count, 0); + break; + case RPM_NUMERIC_CLASS: + escape = 0; + /* fallthrough */ + default: + val = stringFormat(td, emsg); + break; + } + + if (escape) { + char *s = jsonEscape(val); + free(val); + val = s; + } + return val; +} + /* signature fingerprint and time formatting */ static char * pgpsigFormat(rpmtd td, char **emsg) { @@ -545,6 +620,8 @@ static const struct headerFmt_s rpmHeaderFormats[] = { RPM_ANY_CLASS, tagnameFormat }, { RPMTD_FORMAT_TAGNUM, "tagnum", RPM_ANY_CLASS, tagnumFormat }, + { RPMTD_FORMAT_JSON, "json", + RPM_ANY_CLASS, jsonFormat }, { -1, NULL, 0, NULL } }; diff --git a/lib/headerfmt.c b/lib/headerfmt.c index c7802a4922..9601e19ffc 100644 --- a/lib/headerfmt.c +++ b/lib/headerfmt.c @@ -301,6 +301,66 @@ static struct xformat_s xformat_xml = { .xTagFooter = xmlTagFooter, }; +static void jsonHeader(headerSprintfArgs hsa) +{ + hsaAppend(hsa, "{\n"); +} + +static void jsonFooter(headerSprintfArgs hsa) +{ + /* + * XXX HACK: when iterating through the header tags, we don't know + * which element will be the last one. We have to emit ',' for each + * tag we process and then delete the last one here. + */ + hsa->vallen -= 2; + hsaAppend(hsa, "\n}\n"); +} + +static void jsonTagHeader(headerSprintfArgs hsa, rpmTagVal tag, int nelem) +{ + char *tagname = tagName(tag); + hsaAppend(hsa, " \""); + hsaAppend(hsa, tagname); + hsaAppend(hsa, "\": "); + if (nelem > 1) + hsaAppend(hsa, "[\n"); + free(tagname); +} + +static void jsonTagFooter(headerSprintfArgs hsa, rpmTagVal tag, int nelem) +{ + if (nelem > 1) + hsaAppend(hsa, " ]"); + hsaAppend(hsa, ","); + hsaAppend(hsa, "\n"); +} + +static void jsonItemHeader(headerSprintfArgs hsa, rpmTagVal tag, + int n, int nelem) +{ + hsaAppend(hsa, "\t"); +} + +static void jsonItemFooter(headerSprintfArgs hsa, rpmTagVal tag, + int n, int nelem) +{ + if (nelem > 1) { + if (n < nelem-1) + hsaAppend(hsa, ","); + hsaAppend(hsa, "\n"); + } +} + +static struct xformat_s xformat_json = { + .xHeader = jsonHeader, + .xFooter = jsonFooter, + .xTagHeader = jsonTagHeader, + .xTagFooter = jsonTagFooter, + .xItemHeader = jsonItemHeader, + .xItemFooter = jsonItemFooter, +}; + /** * Search tags for a name. * @param hsa headerSprintf args @@ -886,6 +946,8 @@ char * headerFormat(Header h, const char * fmt, errmsg_t * errmsg) if (tag != NULL && tag->tag == -2 && tag->type != NULL) { if (rstreq(tag->type, "xml")) hsa.xfmt = xformat_xml; /* struct assignment */ + else if (rstreq(tag->type, "json")) + hsa.xfmt = xformat_json; /* struct assignment */ } if (hsa.xfmt.xHeader) diff --git a/rpmpopt.in b/rpmpopt.in index 2e9ffb157d..3d718e9868 100644 --- a/rpmpopt.in +++ b/rpmpopt.in @@ -119,6 +119,9 @@ rpm alias --changes --qf '[* %{CHANGELOGTIME:date} %{CHANGELOGNAME}\n%{CHANGELOG rpm alias --xml --qf '[%{*:xml}\n]' \ --POPTdesc=$"list metadata in xml" +rpm alias --json --qf '[%{*:json}]' \ + --POPTdesc=$"list metadata in JSON" + rpm alias --triggerscripts --qf '\ [trigger%{TRIGGERTYPE} scriptlet (using %{TRIGGERSCRIPTPROG}) -- %{TRIGGERCONDS}\n\ %{TRIGGERSCRIPTS}\n]' diff --git a/tests/rpmquery.at b/tests/rpmquery.at index d7628eec72..2ec04dd1f8 100644 --- a/tests/rpmquery.at +++ b/tests/rpmquery.at @@ -943,6 +943,268 @@ runroot rpm -qp --xml /data/RPMS/hello-2.0-1.x86_64.rpm []) RPMTEST_CLEANUP +AT_SETUP([json format 1]) +AT_KEYWORDS([query]) +RPMTEST_CHECK([ +RPMDB_INIT +runroot rpm -qp --json /data/RPMS/hello-2.0-1.x86_64.rpm +], +[0], +[[{ + "Headeri18ntable": "C", + "Sigsize": 5411, + "Sigmd5": "E3yh2LNcygKhhUujAcVDLg==", + "Sha1header": "5cd9874c510b67b44483f9e382a1649ef7743bac", + "Sha256header": "ef920781af3bf072ae9888eec3de1c589143101dff9cc0b561468d395fb766d9", + "Name": "hello", + "Version": "2.0", + "Release": "1", + "Summary": "hello -- hello, world rpm", + "Description": "Simple rpm demonstration.", + "Buildtime": 1227355200, + "Buildhost": "localhost", + "Size": 7243, + "License": "GPL", + "Group": "Testing", + "Os": "linux", + "Arch": "x86_64", + "Filesizes": [ + 7120, + 4096, + 48, + 36, + 39 + ], + "Filemodes": [ + 33257, + 16877, + 33188, + 33188, + 33188 + ], + "Filerdevs": [ + 0, + 0, + 0, + 0, + 0 + ], + "Filemtimes": [ + 1489670606, + 1489670606, + 908894882, + 908895030, + 908884468 + ], + "Filedigests": [ + "c89fa87aeb1143969c0b6be9334b21d932f77f74e8f60120b5de316406369cf0", + "", + "fac3b28492ecdc16da172a6f1a432ceed356ca4d9248157b2a962b395e37b3b0", + "678b87e217a415f05e43460e2c7b668245b412e2b4f18a75aa7399d9774ed0b4", + "d63fdc6c986106f57230f217d36b2395d83ecf491d2b7187af714dc8db9629e9" + ], + "Filelinktos": [ + "", + "", + "", + "", + "" + ], + "Fileflags": [ + 0, + 0, + 2, + 2, + 2 + ], + "Fileusername": [ + "root", + "root", + "root", + "root", + "root" + ], + "Filegroupname": [ + "root", + "root", + "root", + "root", + "root" + ], + "Sourcerpm": "hello-2.0-1.src.rpm", + "Fileverifyflags": [ + 4294967295, + 0, + 4294967295, + 4294967295, + 4294967295 + ], + "Archivesize": 8060, + "Providename": [ + "hello", + "hello(x86-64)" + ], + "Requireflags": [ + 16384, + 16384, + 16777226, + 16777226, + 16777226, + 16384 + ], + "Requirename": [ + "libc.so.6()(64bit)", + "libc.so.6(GLIBC_2.2.5)(64bit)", + "rpmlib(CompressedFileNames)", + "rpmlib(FileDigests)", + "rpmlib(PayloadFilesHavePrefix)", + "rtld(GNU_HASH)" + ], + "Requireversion": [ + "", + "", + "3.0.4-1", + "4.6.0-1", + "4.0-1", + "" + ], + "Rpmversion": "4.13.90", + "Changelogtime": [ + 1227355200, + 908884800 + ], + "Changelogname": [ + "Panu Matilainen ", + "Jeff Johnson " + ], + "Changelogtext": [ + "- updated version", + "- create." + ], + "Filedevices": [ + 1, + 1, + 1, + 1, + 1 + ], + "Fileinodes": [ + 1, + 2, + 3, + 4, + 5 + ], + "Filelangs": [ + "", + "", + "", + "", + "" + ], + "Prefixes": "/usr", + "Provideflags": [ + 8, + 8 + ], + "Provideversion": [ + "2.0-1", + "2.0-1" + ], + "Dirindexes": [ + 0, + 1, + 2, + 2, + 2 + ], + "Basenames": [ + "hello", + "hello-2.0", + "COPYING", + "FAQ", + "README" + ], + "Dirnames": [ + "/usr/bin/", + "/usr/share/doc/", + "/usr/share/doc/hello-2.0/" + ], + "Optflags": "-O2 -g -pipe -Wall -Werror=format-security -Wp,-D_FORTIFY_SOURCE=2 -fexceptions -fstack-protector-strong --param=ssp-buffer-size=4 -grecord-gcc-switches -specs=/usr/lib/rpm/redhat/redhat-hardened-cc1 -m64 -mtune=generic", + "Payloadformat": "cpio", + "Payloadcompressor": "gzip", + "Payloadflags": "9", + "Platform": "x86_64-redhat-linux-gnu", + "Filecolors": [ + 2, + 0, + 0, + 0, + 0 + ], + "Fileclass": [ + 0, + 1, + 2, + 2, + 2 + ], + "Classdict": [ + "ELF 64-bit LSB executable, x86-64, version 1 (SYSV), dynamically linked, interpreter /lib64/ld-linux-x86-64.so.2, for GNU/Linux 2.6.32, BuildID[sha1]=775fdcb927e4300adbe83cfacec3cfeb1f63fe17, stripped", + "directory", + "ASCII text" + ], + "Filedependsx": [ + 0, + 0, + 0, + 0, + 0 + ], + "Filedependsn": [ + 3, + 0, + 0, + 0, + 0 + ], + "Dependsdict": [ + 1375731713, + 1375731712, + 1375731717 + ], + "Filedigestalgo": 8, + "Encoding": "utf-8", + "Payloaddigest": "84a7338287bf19715c4eed0243f5cdb447eeb0ade37b2af718d4060aefca2f7c", + "Payloaddigestalgo": 8 +} +]], +[]) +RPMTEST_CLEANUP + +AT_SETUP([json format 2]) +AT_KEYWORDS([query]) +RPMDB_INIT +runroot rpmbuild --quiet -bb /data/SPECS/weirdnames.spec + +RPMTEST_CHECK([ +runroot rpm -q --json /build/RPMS/noarch/weirdnames-1.0-1.noarch.rpm +], +[0], +[stdout], +[]) + +RPMPY_CHECK([ +import json +s = open('stdout').read() +print(len(json.loads(s))) +], +[58 +], +[]) +RPMTEST_CLEANUP + + AT_SETUP([query file attribute filtering]) AT_KEYWORDS([query]) RPMTEST_CHECK([