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