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