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