1 /* -*- Mode: C; tab-width: 8; indent-tabs-mode: t; c-basic-offset: 8 -*-
2  *
3  * Copyright (C) 2014-2016 Richard Hughes <richard@hughsie.com>
4  * Copyright (C) 2016 Alexander Larsson <alexl@redhat.com>
5  *
6  * SPDX-License-Identifier: GPL-2.0+
7  */
8 
9 #include "config.h"
10 
11 #include <appstream-glib.h>
12 #include <glib.h>
13 #include <glib/gi18n.h>
14 #include <stdlib.h>
15 #include <locale.h>
16 #include <errno.h>
17 
18 G_GNUC_PRINTF (2, 3)
19 static void
as_compose_app_log(AsApp * app,const gchar * fmt,...)20 as_compose_app_log (AsApp *app, const gchar *fmt, ...)
21 {
22 	const gchar *id;
23 	gsize i;
24 	va_list args;
25 	g_autofree gchar *tmp = NULL;
26 
27 	va_start (args, fmt);
28 	tmp = g_strdup_vprintf (fmt, args);
29 	va_end (args);
30 
31 	/* print status */
32 	id = as_app_get_id (app);
33 	g_print ("%s: ", id);
34 	for (i = strlen (id) + 2; i < 35; i++)
35 		g_print (" ");
36 	g_print ("%s\n", tmp);
37 }
38 
39 static gboolean
add_icons(AsApp * app,const gchar * icons_dir,guint min_icon_size,const gchar * prefix,const gchar * key,GError ** error)40 add_icons (AsApp *app,
41 	   const gchar *icons_dir,
42 	   guint min_icon_size,
43 	   const gchar *prefix,
44 	   const gchar *key,
45 	   GError **error)
46 {
47 	g_autofree gchar *fn_hidpi = NULL;
48 	g_autofree gchar *fn = NULL;
49 	g_autofree gchar *name_hidpi = NULL;
50 	g_autofree gchar *name = NULL;
51 	g_autofree gchar *icon_path = NULL;
52 	g_autofree gchar *icon_subdir = NULL;
53 	g_autofree gchar *icon_path_hidpi = NULL;
54 	g_autofree gchar *icon_subdir_hidpi = NULL;
55 	g_autoptr(AsIcon) icon_hidpi = NULL;
56 	g_autoptr(AsIcon) icon = NULL;
57 	g_autoptr(AsImage) im = NULL;
58 	g_autoptr(GdkPixbuf) pixbuf_hidpi = NULL;
59 	g_autoptr(GdkPixbuf) pixbuf = NULL;
60 	g_autoptr(GError) error_local = NULL;
61 
62 	/* find 64x64 icon */
63 	fn = as_utils_find_icon_filename_full (prefix, key,
64 					       AS_UTILS_FIND_ICON_NONE,
65 					       error);
66 	if (fn == NULL) {
67 		g_prefix_error (error, "Failed to find icon: ");
68 		return FALSE;
69 	}
70 
71 	/* load the icon */
72 	im = as_image_new ();
73 	if (!as_image_load_filename_full (im, fn,
74 					  64, min_icon_size,
75 					  AS_IMAGE_LOAD_FLAG_ALWAYS_RESIZE |
76 					  AS_IMAGE_LOAD_FLAG_ONLY_SUPPORTED |
77 					  AS_IMAGE_LOAD_FLAG_SHARPEN,
78 					  error)) {
79 		g_prefix_error (error, "Failed to load icon: ");
80 		return FALSE;
81 	}
82 	pixbuf = g_object_ref (as_image_get_pixbuf (im));
83 
84 	/* save in target directory */
85 	name = g_strdup_printf ("%s.png", as_app_get_id_filename (AS_APP (app)));
86 
87 	icon = as_icon_new ();
88 	as_icon_set_pixbuf (icon, pixbuf);
89 	as_icon_set_name (icon, name);
90 	as_icon_set_kind (icon, AS_ICON_KIND_CACHED);
91 	as_icon_set_prefix (icon, as_app_get_icon_path (AS_APP (app)));
92 	as_app_add_icon (AS_APP (app), icon);
93 
94 	icon_path = g_build_filename (icons_dir, "64x64", name, NULL);
95 
96 	icon_subdir = g_path_get_dirname (icon_path);
97 	if (g_mkdir_with_parents (icon_subdir, 0755)) {
98 		int errsv = errno;
99 		g_set_error (error,
100 			     AS_APP_ERROR,
101 			     AS_APP_ERROR_FAILED,
102 			     "failed to create %s: %s",
103 			     icon_subdir,
104 			     strerror (errsv));
105 		return FALSE;
106 	}
107 
108 	/* TRANSLATORS: we've saving the icon file to disk */
109 	g_print ("%s %s\n", _("Saving icon"), icon_path);
110 	if (!gdk_pixbuf_save (pixbuf, icon_path, "png", error, NULL))
111 		return FALSE;
112 
113 	/* try to get a HiDPI icon */
114 	fn_hidpi = as_utils_find_icon_filename_full (prefix, key,
115 						     AS_UTILS_FIND_ICON_HI_DPI,
116 						     NULL);
117 	if (fn_hidpi == NULL) {
118 		g_debug ("no HiDPI icon found with key %s in %s", key, prefix);
119 		return TRUE;
120 	}
121 
122 	/* load the HiDPI icon */
123 	g_debug ("trying to load %s", fn_hidpi);
124 	if (!as_image_load_filename_full (im, fn_hidpi,
125 					  128, 128,
126 					  AS_IMAGE_LOAD_FLAG_ALWAYS_RESIZE |
127 					  AS_IMAGE_LOAD_FLAG_SHARPEN,
128 					  &error_local)) {
129 		g_debug ("failed to load HiDPI icon: %s", error_local->message);
130 		return TRUE;
131 	}
132 	pixbuf_hidpi = g_object_ref (as_image_get_pixbuf (im));
133 	if (gdk_pixbuf_get_width (pixbuf_hidpi) <= gdk_pixbuf_get_width (pixbuf) ||
134 	    gdk_pixbuf_get_height (pixbuf_hidpi) <= gdk_pixbuf_get_height (pixbuf)) {
135 		g_debug ("HiDPI icon no larger than normal icon");
136 		return TRUE;
137 	}
138 	as_app_add_kudo_kind (AS_APP (app), AS_KUDO_KIND_HI_DPI_ICON);
139 
140 	/* save icon */
141 	name_hidpi = g_strdup_printf ("%s.png", as_app_get_id_filename (AS_APP (app)));
142 	icon_hidpi = as_icon_new ();
143 	as_icon_set_pixbuf (icon_hidpi, pixbuf_hidpi);
144 	as_icon_set_name (icon_hidpi, name_hidpi);
145 	as_icon_set_kind (icon_hidpi, AS_ICON_KIND_CACHED);
146 	as_icon_set_prefix (icon_hidpi, as_app_get_icon_path (AS_APP (app)));
147 	as_app_add_icon (AS_APP (app), icon_hidpi);
148 
149 	icon_path_hidpi = g_build_filename (icons_dir, "128x128", name_hidpi, NULL);
150 	icon_subdir_hidpi = g_path_get_dirname (icon_path_hidpi);
151 	if (g_mkdir_with_parents (icon_subdir_hidpi, 0755)) {
152 		int errsv = errno;
153 		g_set_error (error,
154 			     AS_APP_ERROR,
155 			     AS_APP_ERROR_FAILED,
156 			     "failed to create %s: %s",
157 			     icon_subdir_hidpi,
158 			     strerror (errsv));
159 		return FALSE;
160 	}
161 
162 	/* TRANSLATORS: we've saving the icon file to disk */
163 	g_print ("%s %s\n", _("Saving icon"), icon_path_hidpi);
164 	if (!gdk_pixbuf_save (pixbuf_hidpi, icon_path_hidpi, "png", error, NULL))
165 		return FALSE;
166 	return TRUE;
167 }
168 
169 static AsApp *
load_desktop(const gchar * prefix,const gchar * icons_dir,guint min_icon_size,const gchar * app_name,const gchar * desktop_path,GError ** error)170 load_desktop (const gchar *prefix,
171 	      const gchar *icons_dir,
172 	      guint        min_icon_size,
173 	      const gchar *app_name,
174 	      const gchar *desktop_path,
175 	      GError **error)
176 {
177 	AsIcon *icon;
178 	g_autoptr(AsApp) app = NULL;
179 
180 	app = as_app_new ();
181 	if (!as_app_parse_file (app, desktop_path,
182 				AS_APP_PARSE_FLAG_USE_HEURISTICS |
183 				AS_APP_PARSE_FLAG_ALLOW_VETO,
184 				error))
185 		return NULL;
186 	if (as_app_get_kind (app) == AS_APP_KIND_UNKNOWN) {
187 		g_set_error (error,
188 			     AS_APP_ERROR,
189 			     AS_APP_ERROR_FAILED,
190 			     "%s has no recognised type",
191 			     as_app_get_id (AS_APP (app)));
192 		return NULL;
193 	}
194 
195 	icon = as_app_get_icon_default (AS_APP (app));
196 	if (icon != NULL) {
197 		g_autofree gchar *key = NULL;
198 		key = g_strdup (as_icon_get_name (icon));
199 		if (as_icon_get_kind (icon) == AS_ICON_KIND_STOCK) {
200 			as_compose_app_log (app,
201 					    "using stock icon %s", key);
202 		} else {
203 			g_autoptr(GError) error_local = NULL;
204 			gboolean ret;
205 
206 			g_ptr_array_set_size (as_app_get_icons (AS_APP (app)), 0);
207 			ret = add_icons (app,
208 					 icons_dir,
209 					 min_icon_size,
210 					 prefix,
211 					 key,
212 					 error);
213 			if (!ret)
214 				return NULL;
215 		}
216 	}
217 
218 	return g_steal_pointer (&app);
219 }
220 
221 static gchar *
get_appdata_filename(const gchar * prefix,const gchar * app_name)222 get_appdata_filename (const gchar *prefix, const gchar *app_name)
223 {
224 	const gchar *dirs[] = { "metainfo", "appdata", NULL };
225 	const gchar *exts[] = { ".metainfo.xml", ".appdata.xml", NULL };
226 
227 	/* fall back to the legacy path and extensions */
228 	for (guint j = 0; dirs[j] != NULL; j++) {
229 		for (guint i = 0; exts[i] != NULL; i++) {
230 			g_autofree gchar *basename = NULL;
231 			g_autofree gchar *tmp = NULL;
232 			basename = g_strconcat (app_name, exts[i], NULL);
233 			tmp = g_build_filename (prefix,
234 						"share",
235 						dirs[j],
236 						basename,
237 						NULL);
238 			if (g_file_test (tmp, G_FILE_TEST_EXISTS))
239 				return g_steal_pointer (&tmp);
240 		}
241 	}
242 	return NULL;
243 }
244 
245 static AsApp *
load_appdata(const gchar * prefix,const gchar * app_name,GError ** error)246 load_appdata (const gchar *prefix, const gchar *app_name, GError **error)
247 {
248 	g_autofree gchar *appdata_path = NULL;
249 	g_autoptr(AsApp) app = NULL;
250 	g_autoptr(GPtrArray) problems = NULL;
251 	AsProblemKind problem_kind;
252 	AsProblem *problem;
253 	guint i;
254 
255 	appdata_path = get_appdata_filename (prefix, app_name);
256 	if (appdata_path == NULL) {
257 		g_set_error (error,
258 			     AS_APP_ERROR,
259 			     AS_APP_ERROR_FAILED,
260 			     "no file found for %s", app_name);
261 		return NULL;
262 	}
263 	g_debug ("looking for appdata path '%s'", appdata_path);
264 
265 	app = as_app_new ();
266 	if (!as_app_parse_file (app, appdata_path,
267 				AS_APP_PARSE_FLAG_USE_HEURISTICS,
268 				error))
269 		return NULL;
270 	if (as_app_get_kind (app) == AS_APP_KIND_UNKNOWN) {
271 		g_set_error (error,
272 			     AS_APP_ERROR,
273 			     AS_APP_ERROR_FAILED,
274 			     "%s has no recognised type",
275 			     as_app_get_id (AS_APP (app)));
276 		return NULL;
277 	}
278 
279 	problems = as_app_validate (app,
280 				    AS_APP_VALIDATE_FLAG_NO_NETWORK |
281 				    AS_APP_VALIDATE_FLAG_RELAX,
282 				    error);
283 	if (problems == NULL)
284 		return NULL;
285 	for (i = 0; i < problems->len; i++) {
286 		problem = g_ptr_array_index (problems, i);
287 		problem_kind = as_problem_get_kind (problem);
288 		as_compose_app_log (app,
289 				    "AppData problem: %s : %s",
290 				    as_problem_kind_to_string (problem_kind),
291 				    as_problem_get_message (problem));
292 	}
293 	if (problems->len > 0) {
294 		g_set_error (error,
295 			     AS_APP_ERROR,
296 			     AS_APP_ERROR_FAILED,
297 			     "AppData file %s was not valid",
298 			     appdata_path);
299 		return NULL;
300 	}
301 
302 	return g_steal_pointer (&app);
303 }
304 
305 int
main(int argc,char ** argv)306 main (int argc, char **argv)
307 {
308 	g_autoptr(GOptionContext) option_context = NULL;
309 	gboolean ret;
310 	gboolean verbose = FALSE;
311 	g_autoptr(GError) error = NULL;
312 	g_autofree gchar *basename = NULL;
313 	g_autofree gchar *icons_dir = NULL;
314 	g_autofree gchar *origin = NULL;
315 	g_autofree gchar *xml_basename = NULL;
316 	g_autofree gchar *output_dir = NULL;
317 	g_autofree gchar *prefix = NULL;
318 	g_autoptr(AsStore) store = NULL;
319 	g_autoptr(GFile) xml_dir = NULL;
320 	g_autoptr(GFile) xml_file = NULL;
321 	guint min_icon_size = 32;
322 	guint i;
323 	const GOptionEntry options[] = {
324 		{ "verbose", 'v', 0, G_OPTION_ARG_NONE, &verbose,
325 			/* TRANSLATORS: command line option */
326 			_("Show extra debugging information"), NULL },
327 		{ "prefix", '\0', 0, G_OPTION_ARG_FILENAME, &prefix,
328 			/* TRANSLATORS: command line option */
329 			_("Set the prefix"), "DIR" },
330 		{ "output-dir", '\0', 0, G_OPTION_ARG_FILENAME, &output_dir,
331 			/* TRANSLATORS: command line option */
332 			_("Set the output directory"), "DIR" },
333 		{ "icons-dir", '\0', 0, G_OPTION_ARG_FILENAME, &icons_dir,
334 			/* TRANSLATORS: command line option */
335 			_("Set the icons directory"), "DIR" },
336 		{ "origin", '\0', 0, G_OPTION_ARG_STRING, &origin,
337 			/* TRANSLATORS: command line option */
338 			_("Set the origin name"), "NAME" },
339 		{ "min-icon-size", '\0', 0, G_OPTION_ARG_INT, &min_icon_size,
340 			/* TRANSLATORS: command line option */
341 			_("Set the minimum icon size in pixels"), "ICON_SIZE" },
342 		{ "basename", '\0', 0, G_OPTION_ARG_STRING, &basename,
343 			/* TRANSLATORS: command line option */
344 			_("Set the basenames of the output files"), "NAME" },
345 		{ NULL}
346 	};
347 
348 	setlocale (LC_ALL, "");
349 	bindtextdomain (GETTEXT_PACKAGE, LOCALEDIR);
350 	bind_textdomain_codeset (GETTEXT_PACKAGE, "UTF-8");
351 	textdomain (GETTEXT_PACKAGE);
352 	option_context = g_option_context_new (" - APP-IDS");
353 
354 	g_option_context_add_main_entries (option_context, options, NULL);
355 	ret = g_option_context_parse (option_context, &argc, &argv, &error);
356 	if (!ret) {
357 		/* TRANSLATORS: error message */
358 		g_print ("%s: %s\n", _("Failed to parse arguments"), error->message);
359 		return EXIT_FAILURE;
360 	}
361 
362 	if (verbose)
363 		g_setenv ("G_MESSAGES_DEBUG", "all", TRUE);
364 
365 	/* set defaults */
366 	if (prefix == NULL)
367 		prefix = g_strdup ("/usr");
368 	if (output_dir == NULL)
369 		output_dir = g_build_filename (prefix, "share/app-info/xmls", NULL);
370 	if (icons_dir == NULL)
371 		icons_dir = g_build_filename (prefix, "share/app-info/icons", origin, NULL);
372 	if (origin == NULL) {
373 		g_print ("WARNING: Metadata origin not set, using 'example'\n");
374 		origin = g_strdup ("example");
375 	}
376 	if (basename == NULL)
377 		basename = g_strdup (origin);
378 
379 	if (argc == 1) {
380 		g_autofree gchar *tmp = NULL;
381 		tmp = g_option_context_get_help (option_context, TRUE, NULL);
382 		g_print ("%s", tmp);
383 		return  EXIT_FAILURE;
384 	}
385 
386 	store = as_store_new ();
387 	as_store_set_api_version (store, 0.8);
388 	as_store_set_origin (store, origin);
389 
390 	/* load each application specified */
391 	for (i = 1; i < (guint) argc; i++) {
392 		AsLaunchable *launchable;
393 		const gchar *app_name = argv[i];
394 		g_auto(GStrv) intl_domains = NULL;
395 		g_autoptr(AsApp) app_appdata = NULL;
396 		g_autoptr(AsApp) app_desktop = NULL;
397 		g_autoptr(GString) desktop_basename = NULL;
398 		g_autofree gchar *desktop_path = NULL;
399 
400 		/* TRANSLATORS: we're generating the AppStream data */
401 		g_print ("%s %s\n", _("Processing application"), app_name);
402 
403 		app_appdata = load_appdata (prefix, app_name, &error);
404 		if (app_appdata == NULL) {
405 			/* TRANSLATORS: the .appdata.xml file could not
406 			 * be loaded */
407 			g_print ("%s: %s\n", _("Error loading AppData file"),
408 				 error->message);
409 			return EXIT_FAILURE;
410 		}
411 
412 		/* set translations */
413 		if (!as_app_builder_search_translations (app_appdata,
414 							 prefix,
415 							 25,
416 							 AS_APP_BUILDER_FLAG_NONE,
417 							 NULL,
418 							 &error)) {
419 			/* TRANSLATORS: the .mo files could not be parsed */
420 			g_print ("%s: %s\n", _("Error parsing translations"),
421 				 error->message);
422 			return EXIT_FAILURE;
423 		}
424 
425 		/* auto-add kudos */
426 		if (!as_app_builder_search_kudos (app_appdata,
427 						  prefix,
428 						  AS_APP_BUILDER_FLAG_NONE,
429 						  &error)) {
430 			/* TRANSLATORS: we could not auto-add the kudo */
431 			g_print ("%s: %s\n", _("Error parsing kudos"),
432 				 error->message);
433 			return EXIT_FAILURE;
434 		}
435 
436 		/* auto-add provides */
437 		if (!as_app_builder_search_provides (app_appdata,
438 						     prefix,
439 						     AS_APP_BUILDER_FLAG_NONE,
440 						     &error)) {
441 			/* TRANSLATORS: we could not auto-add the provides */
442 			g_print ("%s: %s\n", _("Error parsing provides"),
443 				 error->message);
444 			return EXIT_FAILURE;
445 		}
446 
447 		as_store_add_app (store, app_appdata);
448 
449 		/* use the ID from the AppData file if it was found */
450 		launchable = as_app_get_launchable_by_kind (app_appdata,
451 							    AS_LAUNCHABLE_KIND_DESKTOP_ID);
452 		if (launchable != NULL) {
453 			desktop_basename = g_string_new (as_launchable_get_value (launchable));
454 		} else {
455 			const gchar *appdata_id = as_app_get_id (app_appdata);
456 
457 			/* append the .desktop suffix if using a new-style name */
458 			desktop_basename = g_string_new (appdata_id != NULL ? appdata_id : app_name);
459 			if (!g_str_has_suffix (desktop_basename->str, ".desktop"))
460 				g_string_append (desktop_basename, ".desktop");
461 		}
462 
463 		if (!g_str_has_suffix (desktop_basename->str, ".desktop")) {
464 			/* TRANSLATORS: not a valid desktop filename */
465 			g_print ("%s: %s\n", _("Invalid desktop filename"),
466 				 desktop_basename->str);
467 			return EXIT_FAILURE;
468 		}
469 
470 		desktop_path = g_build_filename (prefix, "share", "applications",
471 						 desktop_basename->str, NULL);
472 		g_debug ("looking for desktop path '%s'", desktop_path);
473 
474 		if (g_file_test (desktop_path, G_FILE_TEST_EXISTS)) {
475 			app_desktop = load_desktop (prefix,
476 						    icons_dir,
477 						    min_icon_size,
478 						    app_name,
479 						    desktop_path,
480 						    &error);
481 			if (app_desktop == NULL) {
482 				/* TRANSLATORS: the .desktop file could not
483 				 * be loaded */
484 				g_print ("%s: %s\n", _("Error loading desktop file"),
485 					 error->message);
486 				return EXIT_FAILURE;
487 			}
488 
489 			/* if the appdata <name> exists, do not inherit from
490 			 * the desktop file as it may be prefixed */
491 			if (g_hash_table_size (as_app_get_names (app_appdata)) > 0)
492 				g_hash_table_remove_all (as_app_get_names (app_desktop));
493 			if (g_hash_table_size (as_app_get_comments (app_appdata)) > 0)
494 				g_hash_table_remove_all (as_app_get_comments (app_desktop));
495 
496 			/* does the app already exist with a launchable that matches this ID */
497 			if (g_strcmp0 (as_app_get_id (app_appdata), as_app_get_id (app_desktop)) != 0) {
498 				g_debug ("fixing up ID for desktop merge");
499 				as_app_set_id (app_desktop, as_app_get_id (app_appdata));
500 			}
501 
502 			as_store_add_app (store, app_desktop);
503 		}
504 	}
505 
506 	/* create output directory */
507 	if (g_mkdir_with_parents (output_dir, 0755)) {
508 		int errsv = errno;
509 		g_print ("%s: %s\n",
510 			 /* TRANSLATORS: this is when the folder could
511 			  * not be created */
512 			 _("Error creating output directory"),
513 			 strerror (errsv));
514 		return EXIT_FAILURE;
515 	}
516 
517 	xml_dir = g_file_new_for_path (output_dir);
518 	xml_basename = g_strconcat (basename, ".xml.gz", NULL);
519 	xml_file = g_file_get_child (xml_dir, xml_basename);
520 	/* TRANSLATORS: we've saving the XML file to disk */
521 	g_print ("%s %s\n", _("Saving AppStream"),
522 		 g_file_get_path (xml_file));
523 	if (!as_store_to_file (store,
524 			       xml_file,
525 			       AS_NODE_TO_XML_FLAG_FORMAT_MULTILINE |
526 			       AS_NODE_TO_XML_FLAG_FORMAT_INDENT |
527 			       AS_NODE_TO_XML_FLAG_ADD_HEADER,
528 			       NULL, &error)) {
529 		/* TRANSLATORS: this is when the destination file
530 		 * cannot be saved for some reason */
531 		g_print ("%s: %s\n", _("Error saving AppStream file"),
532 			 error->message);
533 		return EXIT_FAILURE;
534 	}
535 
536 	/* TRANSLATORS: information message */
537 	g_print ("%s\n", _("Done!"));
538 
539 	return EXIT_SUCCESS;
540 }
541