/* * repo_appdatadb.c * * Parses AppSteam Data files. * See http://people.freedesktop.org/~hughsient/appdata/ * * * Copyright (c) 2013, Novell Inc. * * This program is licensed under the BSD license, read LICENSE.BSD * for further information */ #include #include #include #include #include #include #include #include #include #include "pool.h" #include "repo.h" #include "util.h" #include "solv_xmlparser.h" #include "repo_appdata.h" enum state { STATE_START, STATE_APPLICATION, STATE_ID, STATE_PKGNAME, STATE_LICENCE, STATE_NAME, STATE_SUMMARY, STATE_DESCRIPTION, STATE_P, STATE_UL, STATE_UL_LI, STATE_OL, STATE_OL_LI, STATE_URL, STATE_GROUP, STATE_KEYWORDS, STATE_KEYWORD, STATE_EXTENDS, NUMSTATES }; static struct solv_xmlparser_element stateswitches[] = { { STATE_START, "applications", STATE_START, 0 }, { STATE_START, "components", STATE_START, 0 }, { STATE_START, "application", STATE_APPLICATION, 0 }, { STATE_START, "component", STATE_APPLICATION, 0 }, { STATE_APPLICATION, "id", STATE_ID, 1 }, { STATE_APPLICATION, "pkgname", STATE_PKGNAME, 1 }, { STATE_APPLICATION, "product_license", STATE_LICENCE, 1 }, { STATE_APPLICATION, "name", STATE_NAME, 1 }, { STATE_APPLICATION, "summary", STATE_SUMMARY, 1 }, { STATE_APPLICATION, "description", STATE_DESCRIPTION, 0 }, { STATE_APPLICATION, "url", STATE_URL, 1 }, { STATE_APPLICATION, "project_group", STATE_GROUP, 1 }, { STATE_APPLICATION, "keywords", STATE_KEYWORDS, 0 }, { STATE_APPLICATION, "extends", STATE_EXTENDS, 1 }, { STATE_DESCRIPTION, "p", STATE_P, 1 }, { STATE_DESCRIPTION, "ul", STATE_UL, 0 }, { STATE_DESCRIPTION, "ol", STATE_OL, 0 }, { STATE_UL, "li", STATE_UL_LI, 1 }, { STATE_OL, "li", STATE_OL_LI, 1 }, { STATE_KEYWORDS, "keyword", STATE_KEYWORD, 1 }, { NUMSTATES } }; struct parsedata { Pool *pool; Repo *repo; Repodata *data; int ret; Solvable *solvable; Id handle; int skiplang; char *description; int licnt; int skip_depth; int flags; char *desktop_file; int havesummary; const char *filename; Queue *owners; struct solv_xmlparser xmlp; }; static void startElement(struct solv_xmlparser *xmlp, int state, const char *name, const char **atts) { struct parsedata *pd = xmlp->userdata; Pool *pool = pd->pool; Solvable *s; const char *type; /* ignore all language tags */ if (pd->skiplang || solv_xmlparser_find_attr("xml:lang", atts)) { pd->skiplang++; return; } switch(state) { case STATE_APPLICATION: type = solv_xmlparser_find_attr("type", atts); if (!type || !*type) type = "desktop"; if (strcmp(type, "desktop") != 0) { /* ignore for now */ pd->solvable = 0; break; } s = pd->solvable = pool_id2solvable(pool, repo_add_solvable(pd->repo)); pd->handle = s - pool->solvables; pd->havesummary = 0; repodata_set_poolstr(pd->data, pd->handle, SOLVABLE_CATEGORY, type); break; case STATE_DESCRIPTION: pd->description = solv_free(pd->description); break; case STATE_OL: case STATE_UL: pd->licnt = 0; break; default: break; } } /* replace whitespace with one space/newline */ /* also strip starting/ending whitespace */ static char * wsstrip(struct parsedata *pd) { struct solv_xmlparser *xmlp = &pd->xmlp; int i, j; int ws = 0; for (i = j = 0; xmlp->content[i]; i++) { if (xmlp->content[i] == ' ' || xmlp->content[i] == '\t' || xmlp->content[i] == '\n') { ws |= xmlp->content[i] == '\n' ? 2 : 1; continue; } if (ws && j) xmlp->content[j++] = (ws & 2) ? '\n' : ' '; ws = 0; xmlp->content[j++] = xmlp->content[i]; } xmlp->content[j] = 0; xmlp->lcontent = j; return xmlp->content; } /* indent all lines */ static char * indent(struct parsedata *pd, int il) { struct solv_xmlparser *xmlp = &pd->xmlp; int i, l; for (l = 0; xmlp->content[l]; ) { if (xmlp->content[l] == '\n') { l++; continue; } if (xmlp->lcontent + il + 1 > xmlp->acontent) { xmlp->acontent = xmlp->lcontent + il + 256; xmlp->content = solv_realloc(xmlp->content, xmlp->acontent); } memmove(xmlp->content + l + il, xmlp->content + l, xmlp->lcontent - l + 1); for (i = 0; i < il; i++) xmlp->content[l + i] = ' '; xmlp->lcontent += il; while (xmlp->content[l] && xmlp->content[l] != '\n') l++; } return xmlp->content; } static void add_missing_tags_from_desktop_file(struct parsedata *pd, Solvable *s, const char *desktop_file) { Pool *pool = pd->pool; FILE *fp; const char *filepath; char buf[1024]; char *p, *p2, *p3; int inde = 0; filepath = pool_tmpjoin(pool, "/usr/share/applications/", desktop_file, 0); if (pd->flags & REPO_USE_ROOTDIR) filepath = pool_prepend_rootdir_tmp(pool, filepath); if (!(fp = fopen(filepath, "r"))) return; while (fgets(buf, sizeof(buf), fp) > 0) { int c, l = strlen(buf); if (!l) continue; if (buf[l - 1] != '\n') { /* ignore overlong lines */ while ((c = getc(fp)) != EOF) if (c == '\n') break; if (c == EOF) break; continue; } buf[--l] = 0; while (l && (buf[l - 1] == ' ' || buf[l - 1] == '\t')) buf[--l] = 0; p = buf; while (*p == ' ' || *p == '\t') p++; if (!*p || *p == '#') continue; if (*p == '[') inde = 0; if (!strcmp(p, "[Desktop Entry]")) { inde = 1; continue; } if (!inde) continue; p2 = strchr(p, '='); if (!p2 || p2 == p) continue; *p2 = 0; for (p3 = p2 - 1; *p3 == ' ' || *p3 == '\t'; p3--) *p3 = 0; p2++; while (*p2 == ' ' || *p2 == '\t') p2++; if (!*p2) continue; if (!s->name && !strcmp(p, "Name")) s->name = pool_str2id(pool, pool_tmpjoin(pool, "application:", p2, 0), 1); else if (!pd->havesummary && !strcmp(p, "Comment")) { pd->havesummary = 1; repodata_set_str(pd->data, pd->handle, SOLVABLE_SUMMARY, p2); } else continue; if (s->name && pd->havesummary) break; /* our work is done */ } fclose(fp); } static char * guess_filename_from_id(Pool *pool, const char *id) { int l = strlen(id); char *r = pool_tmpjoin(pool, id, ".metainfo.xml", 0); if (l > 8 && !strcmp(".desktop", id + l - 8)) strcpy(r + l - 8, ".appdata.xml"); else if (l > 4 && !strcmp(".ttf", id + l - 4)) strcpy(r + l - 4, ".metainfo.xml"); else if (l > 4 && !strcmp(".otf", id + l - 4)) strcpy(r + l - 4, ".metainfo.xml"); else if (l > 4 && !strcmp(".xml", id + l - 4)) strcpy(r + l - 4, ".metainfo.xml"); else if (l > 3 && !strcmp(".db", id + l - 3)) strcpy(r + l - 3, ".metainfo.xml"); else return 0; return r; } static void endElement(struct solv_xmlparser *xmlp, int state, char *content) { struct parsedata *pd = xmlp->userdata; Pool *pool = pd->pool; Solvable *s = pd->solvable; Id id; if (pd->skiplang) { pd->skiplang--; return; } if (!s) return; switch (state) { case STATE_APPLICATION: if (!s->arch) s->arch = ARCH_NOARCH; if (!s->evr) s->evr = ID_EMPTY; if ((!s->name || !pd->havesummary) && (pd->flags & APPDATA_CHECK_DESKTOP_FILE) != 0 && pd->desktop_file) add_missing_tags_from_desktop_file(pd, s, pd->desktop_file); if (!s->name && pd->desktop_file) { char *name = pool_tmpjoin(pool, "application:", pd->desktop_file, 0); int l = strlen(name); if (l > 8 && !strcmp(".desktop", name + l - 8)) l -= 8; s->name = pool_strn2id(pool, name, l, 1); } if (!s->requires && pd->owners) { int i; Id id; for (i = 0; i < pd->owners->count; i++) { Solvable *os = pd->pool->solvables + pd->owners->elements[i]; s->requires = repo_addid_dep(pd->repo, s->requires, os->name, 0); id = pool_str2id(pd->pool, pool_tmpjoin(pd->pool, "application-appdata(", pool_id2str(pd->pool, os->name), ")"), 1); s->provides = repo_addid_dep(pd->repo, s->provides, id, 0); } } if (!s->requires && (pd->desktop_file || pd->filename)) { /* add appdata() link requires/provides */ const char *filename = pd->filename; if (!filename) filename = guess_filename_from_id(pool, pd->desktop_file); if (filename) { filename = pool_tmpjoin(pool, "application-appdata(", filename, ")"); s->requires = repo_addid_dep(pd->repo, s->requires, pool_str2id(pd->pool, filename + 12, 1), 0); s->provides = repo_addid_dep(pd->repo, s->provides, pool_str2id(pd->pool, filename, 1), 0); } } if (s->name && s->arch != ARCH_SRC && s->arch != ARCH_NOSRC) s->provides = repo_addid_dep(pd->repo, s->provides, pool_rel2id(pd->pool, s->name, s->evr, REL_EQ, 1), 0); pd->solvable = 0; pd->desktop_file = solv_free(pd->desktop_file); break; case STATE_ID: pd->desktop_file = solv_strdup(content); break; case STATE_NAME: s->name = pool_str2id(pd->pool, pool_tmpjoin(pool, "application:", content, 0), 1); break; case STATE_LICENCE: repodata_add_poolstr_array(pd->data, pd->handle, SOLVABLE_LICENSE, content); break; case STATE_SUMMARY: pd->havesummary = 1; repodata_set_str(pd->data, pd->handle, SOLVABLE_SUMMARY, content); break; case STATE_URL: repodata_set_str(pd->data, pd->handle, SOLVABLE_URL, content); break; case STATE_GROUP: repodata_add_poolstr_array(pd->data, pd->handle, SOLVABLE_GROUP, content); break; case STATE_EXTENDS: repodata_add_poolstr_array(pd->data, pd->handle, SOLVABLE_EXTENDS, content); break; case STATE_DESCRIPTION: if (pd->description) { /* strip trailing newlines */ int l = strlen(pd->description); while (l && pd->description[l - 1] == '\n') pd->description[--l] = 0; repodata_set_str(pd->data, pd->handle, SOLVABLE_DESCRIPTION, pd->description); } break; case STATE_P: content = wsstrip(pd); pd->description = solv_dupappend(pd->description, content, "\n\n"); break; case STATE_UL_LI: wsstrip(pd); content = indent(pd, 4); content[2] = '-'; pd->description = solv_dupappend(pd->description, content, "\n"); break; case STATE_OL_LI: wsstrip(pd); content = indent(pd, 4); if (++pd->licnt >= 10) content[0] = '0' + (pd->licnt / 10) % 10; content[1] = '0' + pd->licnt % 10; content[2] = '.'; pd->description = solv_dupappend(pd->description, content, "\n"); break; case STATE_UL: case STATE_OL: pd->description = solv_dupappend(pd->description, "\n", 0); break; case STATE_PKGNAME: id = pool_str2id(pd->pool, content, 1); s->requires = repo_addid_dep(pd->repo, s->requires, id, 0); id = pool_str2id(pd->pool, pool_tmpjoin(pd->pool, "application-appdata(", content, ")"), 1); s->provides = repo_addid_dep(pd->repo, s->provides, id, 0); break; case STATE_KEYWORD: repodata_add_poolstr_array(pd->data, pd->handle, SOLVABLE_KEYWORDS, content); break; default: break; } } static int repo_add_appdata_fn(Repo *repo, FILE *fp, int flags, const char *filename, Queue *owners) { Repodata *data; struct parsedata pd; data = repo_add_repodata(repo, flags); memset(&pd, 0, sizeof(pd)); pd.repo = repo; pd.pool = repo->pool; pd.data = data; pd.flags = flags; pd.filename = filename; pd.owners = owners; solv_xmlparser_init(&pd.xmlp, stateswitches, &pd, startElement, endElement); if (solv_xmlparser_parse(&pd.xmlp, fp) != SOLV_XMLPARSER_OK) { pool_debug(pd.pool, SOLV_ERROR, "repo_appdata: %s at line %u:%u\n", pd.xmlp.errstr, pd.xmlp.line, pd.xmlp.column); pd.ret = -1; pd.solvable = solvable_free(pd.solvable, 1); } solv_xmlparser_free(&pd.xmlp); solv_free(pd.desktop_file); solv_free(pd.description); if (!(flags & REPO_NO_INTERNALIZE)) repodata_internalize(data); return pd.ret; } int repo_add_appdata(Repo *repo, FILE *fp, int flags) { return repo_add_appdata_fn(repo, fp, flags, 0, 0); } struct uninternalized_filelist_data { Id did; Queue *res; }; static int search_uninternalized_filelist_cb(void *cbdata, Solvable *s, Repodata *data, Repokey *key, KeyValue *kv) { struct uninternalized_filelist_data *uf = cbdata; const char *str; Id id; size_t l; if (key->type != REPOKEY_TYPE_DIRSTRARRAY || kv->id != uf->did) return 0; str = kv->str; l = strlen(str); if (l > 12 && strncmp(str + l - 12, ".appdata.xml", 12)) id = pool_str2id(data->repo->pool, str, 1); else if (l > 13 && strncmp(str + l - 13, ".metainfo.xml", 13)) id = pool_str2id(data->repo->pool, str, 1); else return 0; queue_push2(uf->res, s - data->repo->pool->solvables, id); return 0; } static void search_uninternalized_filelist(Repo *repo, const char *dir, Queue *res) { Pool *pool = repo->pool; Id did, rdid, p; struct uninternalized_filelist_data uf; uf.res = res; for (rdid = 1; rdid < repo->nrepodata; rdid++) { Repodata *data = repo_id2repodata(repo, rdid); if (!data) continue; if (data->state == REPODATA_STUB) continue; if (!repodata_has_keyname(data, SOLVABLE_FILELIST)) continue; did = repodata_str2dir(data, dir, 0); if (!did) continue; uf.did = did; for (p = data->start; p < data->end; p++) { if (p >= pool->nsolvables || pool->solvables[p].repo != repo) continue; repodata_search_uninternalized(data, p, SOLVABLE_FILELIST, 0, search_uninternalized_filelist_cb, &uf); } } } /* add all files ending in .appdata.xml */ int repo_add_appdata_dir(Repo *repo, const char *appdatadir, int flags) { DIR *dir; char *dirpath; Repodata *data; Queue flq; Queue oq; queue_init(&flq); queue_init(&oq); if (flags & APPDATA_SEARCH_UNINTERNALIZED_FILELIST) search_uninternalized_filelist(repo, appdatadir, &flq); data = repo_add_repodata(repo, flags); if (flags & REPO_USE_ROOTDIR) dirpath = pool_prepend_rootdir(repo->pool, appdatadir); else dirpath = solv_strdup(appdatadir); if ((dir = opendir(dirpath)) != 0) { struct dirent *entry; while ((entry = readdir(dir))) { const char *n; FILE *fp; int len = strlen(entry->d_name); if (entry->d_name[0] == '.') continue; if (!(len > 12 && !strcmp(entry->d_name + len - 12, ".appdata.xml")) && !(len > 13 && !strcmp(entry->d_name + len - 13, ".metainfo.xml"))) continue; n = pool_tmpjoin(repo->pool, dirpath, "/", entry->d_name); fp = fopen(n, "r"); if (!fp) { pool_error(repo->pool, 0, "%s: %s", n, strerror(errno)); continue; } if (flags & APPDATA_SEARCH_UNINTERNALIZED_FILELIST) { Id id = pool_str2id(repo->pool, entry->d_name, 0); queue_empty(&oq); if (id) { int i; for (i = 0; i < flq.count; i += 2) if (flq.elements[i + 1] == id) queue_push(&oq, flq.elements[i]); } } repo_add_appdata_fn(repo, fp, flags | REPO_NO_INTERNALIZE | REPO_REUSE_REPODATA | APPDATA_CHECK_DESKTOP_FILE, entry->d_name, oq.count ? &oq : 0); fclose(fp); } closedir(dir); } solv_free(dirpath); if (!(flags & REPO_NO_INTERNALIZE)) repodata_internalize(data); queue_free(&oq); queue_free(&flq); return 0; }