1 /*
2 * repo_appdatadb.c
3 *
4 * Parses AppSteam Data files.
5 * See http://people.freedesktop.org/~hughsient/appdata/
6 *
7 *
8 * Copyright (c) 2013, Novell Inc.
9 *
10 * This program is licensed under the BSD license, read LICENSE.BSD
11 * for further information
12 */
13
14 #include <sys/types.h>
15 #include <sys/stat.h>
16 #include <unistd.h>
17 #include <stdio.h>
18 #include <stdlib.h>
19 #include <string.h>
20 #include <assert.h>
21 #include <dirent.h>
22 #include <errno.h>
23
24 #include "pool.h"
25 #include "repo.h"
26 #include "util.h"
27 #include "solv_xmlparser.h"
28 #include "repo_appdata.h"
29
30
31 enum state {
32 STATE_START,
33 STATE_APPLICATION,
34 STATE_ID,
35 STATE_PKGNAME,
36 STATE_LICENCE,
37 STATE_NAME,
38 STATE_SUMMARY,
39 STATE_DESCRIPTION,
40 STATE_P,
41 STATE_UL,
42 STATE_UL_LI,
43 STATE_OL,
44 STATE_OL_LI,
45 STATE_URL,
46 STATE_GROUP,
47 STATE_KEYWORDS,
48 STATE_KEYWORD,
49 STATE_EXTENDS,
50 NUMSTATES
51 };
52
53
54 static struct solv_xmlparser_element stateswitches[] = {
55 { STATE_START, "applications", STATE_START, 0 },
56 { STATE_START, "components", STATE_START, 0 },
57 { STATE_START, "application", STATE_APPLICATION, 0 },
58 { STATE_START, "component", STATE_APPLICATION, 0 },
59 { STATE_APPLICATION, "id", STATE_ID, 1 },
60 { STATE_APPLICATION, "pkgname", STATE_PKGNAME, 1 },
61 { STATE_APPLICATION, "product_license", STATE_LICENCE, 1 },
62 { STATE_APPLICATION, "name", STATE_NAME, 1 },
63 { STATE_APPLICATION, "summary", STATE_SUMMARY, 1 },
64 { STATE_APPLICATION, "description", STATE_DESCRIPTION, 0 },
65 { STATE_APPLICATION, "url", STATE_URL, 1 },
66 { STATE_APPLICATION, "project_group", STATE_GROUP, 1 },
67 { STATE_APPLICATION, "keywords", STATE_KEYWORDS, 0 },
68 { STATE_APPLICATION, "extends", STATE_EXTENDS, 1 },
69 { STATE_DESCRIPTION, "p", STATE_P, 1 },
70 { STATE_DESCRIPTION, "ul", STATE_UL, 0 },
71 { STATE_DESCRIPTION, "ol", STATE_OL, 0 },
72 { STATE_UL, "li", STATE_UL_LI, 1 },
73 { STATE_OL, "li", STATE_OL_LI, 1 },
74 { STATE_KEYWORDS, "keyword", STATE_KEYWORD, 1 },
75 { NUMSTATES }
76 };
77
78 struct parsedata {
79 Pool *pool;
80 Repo *repo;
81 Repodata *data;
82 int ret;
83
84 Solvable *solvable;
85 Id handle;
86
87 int skiplang;
88 char *description;
89 int licnt;
90 int skip_depth;
91 int flags;
92 char *desktop_file;
93 int havesummary;
94 const char *filename;
95 Queue *owners;
96
97 struct solv_xmlparser xmlp;
98 };
99
100
101 static void
startElement(struct solv_xmlparser * xmlp,int state,const char * name,const char ** atts)102 startElement(struct solv_xmlparser *xmlp, int state, const char *name, const char **atts)
103 {
104 struct parsedata *pd = xmlp->userdata;
105 Pool *pool = pd->pool;
106 Solvable *s;
107 const char *type;
108
109 /* ignore all language tags */
110 if (pd->skiplang || solv_xmlparser_find_attr("xml:lang", atts))
111 {
112 pd->skiplang++;
113 return;
114 }
115
116 switch(state)
117 {
118 case STATE_APPLICATION:
119 type = solv_xmlparser_find_attr("type", atts);
120 if (!type || !*type)
121 type = "desktop";
122 if (strcmp(type, "desktop") != 0)
123 {
124 /* ignore for now */
125 pd->solvable = 0;
126 break;
127 }
128 s = pd->solvable = pool_id2solvable(pool, repo_add_solvable(pd->repo));
129 pd->handle = s - pool->solvables;
130 pd->havesummary = 0;
131 repodata_set_poolstr(pd->data, pd->handle, SOLVABLE_CATEGORY, type);
132 break;
133 case STATE_DESCRIPTION:
134 pd->description = solv_free(pd->description);
135 break;
136 case STATE_OL:
137 case STATE_UL:
138 pd->licnt = 0;
139 break;
140 default:
141 break;
142 }
143 }
144
145 /* replace whitespace with one space/newline */
146 /* also strip starting/ending whitespace */
147 static char *
wsstrip(struct parsedata * pd)148 wsstrip(struct parsedata *pd)
149 {
150 struct solv_xmlparser *xmlp = &pd->xmlp;
151 int i, j;
152 int ws = 0;
153 for (i = j = 0; xmlp->content[i]; i++)
154 {
155 if (xmlp->content[i] == ' ' || xmlp->content[i] == '\t' || xmlp->content[i] == '\n')
156 {
157 ws |= xmlp->content[i] == '\n' ? 2 : 1;
158 continue;
159 }
160 if (ws && j)
161 xmlp->content[j++] = (ws & 2) ? '\n' : ' ';
162 ws = 0;
163 xmlp->content[j++] = xmlp->content[i];
164 }
165 xmlp->content[j] = 0;
166 xmlp->lcontent = j;
167 return xmlp->content;
168 }
169
170 /* indent all lines */
171 static char *
indent(struct parsedata * pd,int il)172 indent(struct parsedata *pd, int il)
173 {
174 struct solv_xmlparser *xmlp = &pd->xmlp;
175 int i, l;
176 for (l = 0; xmlp->content[l]; )
177 {
178 if (xmlp->content[l] == '\n')
179 {
180 l++;
181 continue;
182 }
183 if (xmlp->lcontent + il + 1 > xmlp->acontent)
184 {
185 xmlp->acontent = xmlp->lcontent + il + 256;
186 xmlp->content = solv_realloc(xmlp->content, xmlp->acontent);
187 }
188 memmove(xmlp->content + l + il, xmlp->content + l, xmlp->lcontent - l + 1);
189 for (i = 0; i < il; i++)
190 xmlp->content[l + i] = ' ';
191 xmlp->lcontent += il;
192 while (xmlp->content[l] && xmlp->content[l] != '\n')
193 l++;
194 }
195 return xmlp->content;
196 }
197
198 static void
add_missing_tags_from_desktop_file(struct parsedata * pd,Solvable * s,const char * desktop_file)199 add_missing_tags_from_desktop_file(struct parsedata *pd, Solvable *s, const char *desktop_file)
200 {
201 Pool *pool = pd->pool;
202 FILE *fp;
203 const char *filepath;
204 char buf[1024];
205 char *p, *p2, *p3;
206 int inde = 0;
207
208 filepath = pool_tmpjoin(pool, "/usr/share/applications/", desktop_file, 0);
209 if (pd->flags & REPO_USE_ROOTDIR)
210 filepath = pool_prepend_rootdir_tmp(pool, filepath);
211 if (!(fp = fopen(filepath, "r")))
212 return;
213 while (fgets(buf, sizeof(buf), fp) > 0)
214 {
215 int c, l = strlen(buf);
216 if (!l)
217 continue;
218 if (buf[l - 1] != '\n')
219 {
220 /* ignore overlong lines */
221 while ((c = getc(fp)) != EOF)
222 if (c == '\n')
223 break;
224 if (c == EOF)
225 break;
226 continue;
227 }
228 buf[--l] = 0;
229 while (l && (buf[l - 1] == ' ' || buf[l - 1] == '\t'))
230 buf[--l] = 0;
231 p = buf;
232 while (*p == ' ' || *p == '\t')
233 p++;
234 if (!*p || *p == '#')
235 continue;
236 if (*p == '[')
237 inde = 0;
238 if (!strcmp(p, "[Desktop Entry]"))
239 {
240 inde = 1;
241 continue;
242 }
243 if (!inde)
244 continue;
245 p2 = strchr(p, '=');
246 if (!p2 || p2 == p)
247 continue;
248 *p2 = 0;
249 for (p3 = p2 - 1; *p3 == ' ' || *p3 == '\t'; p3--)
250 *p3 = 0;
251 p2++;
252 while (*p2 == ' ' || *p2 == '\t')
253 p2++;
254 if (!*p2)
255 continue;
256 if (!s->name && !strcmp(p, "Name"))
257 s->name = pool_str2id(pool, pool_tmpjoin(pool, "application:", p2, 0), 1);
258 else if (!pd->havesummary && !strcmp(p, "Comment"))
259 {
260 pd->havesummary = 1;
261 repodata_set_str(pd->data, pd->handle, SOLVABLE_SUMMARY, p2);
262 }
263 else
264 continue;
265 if (s->name && pd->havesummary)
266 break; /* our work is done */
267 }
268 fclose(fp);
269 }
270
271 static char *
guess_filename_from_id(Pool * pool,const char * id)272 guess_filename_from_id(Pool *pool, const char *id)
273 {
274 int l = strlen(id);
275 char *r = pool_tmpjoin(pool, id, ".metainfo.xml", 0);
276 if (l > 8 && !strcmp(".desktop", id + l - 8))
277 strcpy(r + l - 8, ".appdata.xml");
278 else if (l > 4 && !strcmp(".ttf", id + l - 4))
279 strcpy(r + l - 4, ".metainfo.xml");
280 else if (l > 4 && !strcmp(".otf", id + l - 4))
281 strcpy(r + l - 4, ".metainfo.xml");
282 else if (l > 4 && !strcmp(".xml", id + l - 4))
283 strcpy(r + l - 4, ".metainfo.xml");
284 else if (l > 3 && !strcmp(".db", id + l - 3))
285 strcpy(r + l - 3, ".metainfo.xml");
286 else
287 return 0;
288 return r;
289 }
290
291 static void
endElement(struct solv_xmlparser * xmlp,int state,char * content)292 endElement(struct solv_xmlparser *xmlp, int state, char *content)
293 {
294 struct parsedata *pd = xmlp->userdata;
295 Pool *pool = pd->pool;
296 Solvable *s = pd->solvable;
297 Id id;
298
299 if (pd->skiplang)
300 {
301 pd->skiplang--;
302 return;
303 }
304 if (!s)
305 return;
306
307 switch (state)
308 {
309 case STATE_APPLICATION:
310 if (!s->arch)
311 s->arch = ARCH_NOARCH;
312 if (!s->evr)
313 s->evr = ID_EMPTY;
314 if ((!s->name || !pd->havesummary) && (pd->flags & APPDATA_CHECK_DESKTOP_FILE) != 0 && pd->desktop_file)
315 add_missing_tags_from_desktop_file(pd, s, pd->desktop_file);
316 if (!s->name && pd->desktop_file)
317 {
318 char *name = pool_tmpjoin(pool, "application:", pd->desktop_file, 0);
319 int l = strlen(name);
320 if (l > 8 && !strcmp(".desktop", name + l - 8))
321 l -= 8;
322 s->name = pool_strn2id(pool, name, l, 1);
323 }
324 if (!s->requires && pd->owners)
325 {
326 int i;
327 Id id;
328 for (i = 0; i < pd->owners->count; i++)
329 {
330 Solvable *os = pd->pool->solvables + pd->owners->elements[i];
331 s->requires = repo_addid_dep(pd->repo, s->requires, os->name, 0);
332 id = pool_str2id(pd->pool, pool_tmpjoin(pd->pool, "application-appdata(", pool_id2str(pd->pool, os->name), ")"), 1);
333 s->provides = repo_addid_dep(pd->repo, s->provides, id, 0);
334 }
335 }
336 if (!s->requires && (pd->desktop_file || pd->filename))
337 {
338 /* add appdata() link requires/provides */
339 const char *filename = pd->filename;
340 if (!filename)
341 filename = guess_filename_from_id(pool, pd->desktop_file);
342 if (filename)
343 {
344 filename = pool_tmpjoin(pool, "application-appdata(", filename, ")");
345 s->requires = repo_addid_dep(pd->repo, s->requires, pool_str2id(pd->pool, filename + 12, 1), 0);
346 s->provides = repo_addid_dep(pd->repo, s->provides, pool_str2id(pd->pool, filename, 1), 0);
347 }
348 }
349 if (s->name && s->arch != ARCH_SRC && s->arch != ARCH_NOSRC)
350 s->provides = repo_addid_dep(pd->repo, s->provides, pool_rel2id(pd->pool, s->name, s->evr, REL_EQ, 1), 0);
351 pd->solvable = 0;
352 pd->desktop_file = solv_free(pd->desktop_file);
353 break;
354 case STATE_ID:
355 pd->desktop_file = solv_strdup(content);
356 break;
357 case STATE_NAME:
358 s->name = pool_str2id(pd->pool, pool_tmpjoin(pool, "application:", content, 0), 1);
359 break;
360 case STATE_LICENCE:
361 repodata_add_poolstr_array(pd->data, pd->handle, SOLVABLE_LICENSE, content);
362 break;
363 case STATE_SUMMARY:
364 pd->havesummary = 1;
365 repodata_set_str(pd->data, pd->handle, SOLVABLE_SUMMARY, content);
366 break;
367 case STATE_URL:
368 repodata_set_str(pd->data, pd->handle, SOLVABLE_URL, content);
369 break;
370 case STATE_GROUP:
371 repodata_add_poolstr_array(pd->data, pd->handle, SOLVABLE_GROUP, content);
372 break;
373 case STATE_EXTENDS:
374 repodata_add_poolstr_array(pd->data, pd->handle, SOLVABLE_EXTENDS, content);
375 break;
376 case STATE_DESCRIPTION:
377 if (pd->description)
378 {
379 /* strip trailing newlines */
380 int l = strlen(pd->description);
381 while (l && pd->description[l - 1] == '\n')
382 pd->description[--l] = 0;
383 repodata_set_str(pd->data, pd->handle, SOLVABLE_DESCRIPTION, pd->description);
384 }
385 break;
386 case STATE_P:
387 content = wsstrip(pd);
388 pd->description = solv_dupappend(pd->description, content, "\n\n");
389 break;
390 case STATE_UL_LI:
391 wsstrip(pd);
392 content = indent(pd, 4);
393 content[2] = '-';
394 pd->description = solv_dupappend(pd->description, content, "\n");
395 break;
396 case STATE_OL_LI:
397 wsstrip(pd);
398 content = indent(pd, 4);
399 if (++pd->licnt >= 10)
400 content[0] = '0' + (pd->licnt / 10) % 10;
401 content[1] = '0' + pd->licnt % 10;
402 content[2] = '.';
403 pd->description = solv_dupappend(pd->description, content, "\n");
404 break;
405 case STATE_UL:
406 case STATE_OL:
407 pd->description = solv_dupappend(pd->description, "\n", 0);
408 break;
409 case STATE_PKGNAME:
410 id = pool_str2id(pd->pool, content, 1);
411 s->requires = repo_addid_dep(pd->repo, s->requires, id, 0);
412 id = pool_str2id(pd->pool, pool_tmpjoin(pd->pool, "application-appdata(", content, ")"), 1);
413 s->provides = repo_addid_dep(pd->repo, s->provides, id, 0);
414 break;
415 case STATE_KEYWORD:
416 repodata_add_poolstr_array(pd->data, pd->handle, SOLVABLE_KEYWORDS, content);
417 break;
418 default:
419 break;
420 }
421 }
422
423 static int
repo_add_appdata_fn(Repo * repo,FILE * fp,int flags,const char * filename,Queue * owners)424 repo_add_appdata_fn(Repo *repo, FILE *fp, int flags, const char *filename, Queue *owners)
425 {
426 Repodata *data;
427 struct parsedata pd;
428
429 data = repo_add_repodata(repo, flags);
430 memset(&pd, 0, sizeof(pd));
431 pd.repo = repo;
432 pd.pool = repo->pool;
433 pd.data = data;
434 pd.flags = flags;
435 pd.filename = filename;
436 pd.owners = owners;
437
438 solv_xmlparser_init(&pd.xmlp, stateswitches, &pd, startElement, endElement);
439 if (solv_xmlparser_parse(&pd.xmlp, fp) != SOLV_XMLPARSER_OK)
440 {
441 pool_debug(pd.pool, SOLV_ERROR, "repo_appdata: %s at line %u:%u\n", pd.xmlp.errstr, pd.xmlp.line, pd.xmlp.column);
442 pd.ret = -1;
443 pd.solvable = solvable_free(pd.solvable, 1);
444 }
445 solv_xmlparser_free(&pd.xmlp);
446
447 solv_free(pd.desktop_file);
448 solv_free(pd.description);
449
450 if (!(flags & REPO_NO_INTERNALIZE))
451 repodata_internalize(data);
452
453 return pd.ret;
454 }
455
456 int
repo_add_appdata(Repo * repo,FILE * fp,int flags)457 repo_add_appdata(Repo *repo, FILE *fp, int flags)
458 {
459 return repo_add_appdata_fn(repo, fp, flags, 0, 0);
460 }
461
462 struct uninternalized_filelist_data {
463 Id did;
464 Queue *res;
465 };
466
467 static int
search_uninternalized_filelist_cb(void * cbdata,Solvable * s,Repodata * data,Repokey * key,KeyValue * kv)468 search_uninternalized_filelist_cb(void *cbdata, Solvable *s, Repodata *data, Repokey *key, KeyValue *kv)
469 {
470 struct uninternalized_filelist_data *uf = cbdata;
471 const char *str;
472 Id id;
473 size_t l;
474 if (key->type != REPOKEY_TYPE_DIRSTRARRAY || kv->id != uf->did)
475 return 0;
476 str = kv->str;
477 l = strlen(str);
478 if (l > 12 && strncmp(str + l - 12, ".appdata.xml", 12))
479 id = pool_str2id(data->repo->pool, str, 1);
480 else if (l > 13 && strncmp(str + l - 13, ".metainfo.xml", 13))
481 id = pool_str2id(data->repo->pool, str, 1);
482 else
483 return 0;
484 queue_push2(uf->res, s - data->repo->pool->solvables, id);
485 return 0;
486 }
487
488 static void
search_uninternalized_filelist(Repo * repo,const char * dir,Queue * res)489 search_uninternalized_filelist(Repo *repo, const char *dir, Queue *res)
490 {
491 Pool *pool = repo->pool;
492 Id did, rdid, p;
493 struct uninternalized_filelist_data uf;
494
495 uf.res = res;
496 for (rdid = 1; rdid < repo->nrepodata; rdid++)
497 {
498 Repodata *data = repo_id2repodata(repo, rdid);
499 if (!data)
500 continue;
501 if (data->state == REPODATA_STUB)
502 continue;
503 if (!repodata_has_keyname(data, SOLVABLE_FILELIST))
504 continue;
505 did = repodata_str2dir(data, dir, 0);
506 if (!did)
507 continue;
508 uf.did = did;
509 for (p = data->start; p < data->end; p++)
510 {
511 if (p >= pool->nsolvables || pool->solvables[p].repo != repo)
512 continue;
513 repodata_search_uninternalized(data, p, SOLVABLE_FILELIST, 0, search_uninternalized_filelist_cb, &uf);
514 }
515 }
516 }
517
518 /* add all files ending in .appdata.xml */
519 int
repo_add_appdata_dir(Repo * repo,const char * appdatadir,int flags)520 repo_add_appdata_dir(Repo *repo, const char *appdatadir, int flags)
521 {
522 DIR *dir;
523 char *dirpath;
524 Repodata *data;
525 Queue flq;
526 Queue oq;
527
528 queue_init(&flq);
529 queue_init(&oq);
530 if (flags & APPDATA_SEARCH_UNINTERNALIZED_FILELIST)
531 search_uninternalized_filelist(repo, appdatadir, &flq);
532 data = repo_add_repodata(repo, flags);
533 if (flags & REPO_USE_ROOTDIR)
534 dirpath = pool_prepend_rootdir(repo->pool, appdatadir);
535 else
536 dirpath = solv_strdup(appdatadir);
537 if ((dir = opendir(dirpath)) != 0)
538 {
539 struct dirent *entry;
540 while ((entry = readdir(dir)))
541 {
542 const char *n;
543 FILE *fp;
544 int len = strlen(entry->d_name);
545 if (entry->d_name[0] == '.')
546 continue;
547 if (!(len > 12 && !strcmp(entry->d_name + len - 12, ".appdata.xml")) &&
548 !(len > 13 && !strcmp(entry->d_name + len - 13, ".metainfo.xml")))
549 continue;
550 n = pool_tmpjoin(repo->pool, dirpath, "/", entry->d_name);
551 fp = fopen(n, "r");
552 if (!fp)
553 {
554 pool_error(repo->pool, 0, "%s: %s", n, strerror(errno));
555 continue;
556 }
557 if (flags & APPDATA_SEARCH_UNINTERNALIZED_FILELIST)
558 {
559 Id id = pool_str2id(repo->pool, entry->d_name, 0);
560 queue_empty(&oq);
561 if (id)
562 {
563 int i;
564 for (i = 0; i < flq.count; i += 2)
565 if (flq.elements[i + 1] == id)
566 queue_push(&oq, flq.elements[i]);
567 }
568 }
569 repo_add_appdata_fn(repo, fp, flags | REPO_NO_INTERNALIZE | REPO_REUSE_REPODATA | APPDATA_CHECK_DESKTOP_FILE, entry->d_name, oq.count ? &oq : 0);
570 fclose(fp);
571 }
572 closedir(dir);
573 }
574 solv_free(dirpath);
575 if (!(flags & REPO_NO_INTERNALIZE))
576 repodata_internalize(data);
577 queue_free(&oq);
578 queue_free(&flq);
579 return 0;
580 }
581