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