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) 2011 Paolo Bacchilega <paobac@src.gnome.org>
5  *
6  * SPDX-License-Identifier: LGPL-2.1+
7  */
8 
9 /**
10  * SECTION:as-utils
11  * @short_description: Helper functions that are used inside libappstream-glib
12  * @include: appstream-glib.h
13  * @stability: Stable
14  *
15  * These functions are used internally to libappstream-glib, and some may be
16  * useful to user-applications.
17  */
18 
19 #include "config.h"
20 
21 #include <string.h>
22 #include <archive_entry.h>
23 #include <archive.h>
24 #include <libsoup/soup.h>
25 #include <stdlib.h>
26 #ifndef _WIN32
27 #ifdef __APPLE__
28 #include <uuid/uuid.h>
29 #else
30 #include <uuid.h>
31 #endif
32 #endif
33 
34 #ifdef HAVE_RPM
35 #include <rpm/rpmlib.h>
36 #endif
37 
38 #include "as-app-private.h"
39 #include "as-enums.h"
40 #include "as-node.h"
41 #include "as-resources.h"
42 #include "as-store.h"
43 #include "as-utils.h"
44 #include "as-utils-private.h"
45 
46 /**
47  * as_utils_error_quark:
48  *
49  * Return value: An error quark.
50  *
51  * Since: 0.3.7
52  **/
53 G_DEFINE_QUARK (as-utils-error-quark, as_utils_error)
54 
55 /**
56  * as_hash_lookup_by_locale:
57  * @hash: a #GHashTable.
58  * @locale: the locale, or %NULL to use the users default local.
59  *
60  * Gets the 'best' data entry in a hash table using the user-set list
61  * of preferred languages.
62  *
63  * This is how methods like as_app_get_name(app,NULL) return the localized
64  * data for the user.
65  *
66  * Returns: the string value, or %NULL if there was no data
67  *
68  * Since: 0.1.0
69  **/
70 const gchar *
as_hash_lookup_by_locale(GHashTable * hash,const gchar * locale)71 as_hash_lookup_by_locale (GHashTable *hash, const gchar *locale)
72 {
73 	const gchar *const *locales;
74 	const gchar *tmp = NULL;
75 	guint i;
76 
77 	g_return_val_if_fail (hash != NULL, NULL);
78 
79 	/* the user specified a locale */
80 	if (locale != NULL)
81 		return g_hash_table_lookup (hash, locale);
82 
83 	/* use LANGUAGE, LC_ALL, LC_MESSAGES and LANG */
84 	locales = g_get_language_names ();
85 	for (i = 0; locales[i] != NULL; i++) {
86 		tmp = g_hash_table_lookup (hash, locales[i]);
87 		if (tmp != NULL)
88 			return tmp;
89 	}
90 	return NULL;
91 }
92 
93 static gchar *
as_utils_locale_to_language(const gchar * locale)94 as_utils_locale_to_language (const gchar *locale)
95 {
96 	gchar *tmp;
97 	gchar *country_code;
98 
99 	/* invalid */
100 	if (locale == NULL)
101 		return NULL;
102 
103 	/* return the part before the _ (not always 2 chars!) */
104 	country_code = g_strdup (locale);
105 	tmp = g_strstr_len (country_code, -1, "_");
106 	if (tmp != NULL)
107 		*tmp = '\0';
108 	return country_code;
109 }
110 
111 /**
112  * as_utils_locale_is_compatible:
113  * @locale1: a locale string, or %NULL
114  * @locale2: a locale string, or %NULL
115  *
116  * Calculates if one locale is compatible with another.
117  * When doing the calculation the locale and language code is taken into
118  * account if possible.
119  *
120  * Returns: %TRUE if the locale is compatible.
121  *
122  * Since: 0.5.14
123  **/
124 gboolean
as_utils_locale_is_compatible(const gchar * locale1,const gchar * locale2)125 as_utils_locale_is_compatible (const gchar *locale1, const gchar *locale2)
126 {
127 	g_autofree gchar *lang1 = as_utils_locale_to_language (locale1);
128 	g_autofree gchar *lang2 = as_utils_locale_to_language (locale2);
129 
130 	/* we've specified "don't care" and locale unspecified */
131 	if (locale1 == NULL && locale2 == NULL)
132 		return TRUE;
133 
134 	/* forward */
135 	if (locale1 == NULL && locale2 != NULL) {
136 		const gchar *const *locales = g_get_language_names ();
137 		return g_strv_contains (locales, locale2) ||
138 		       g_strv_contains (locales, lang2);
139 	}
140 
141 	/* backwards */
142 	if (locale1 != NULL && locale2 == NULL) {
143 		const gchar *const *locales = g_get_language_names ();
144 		return g_strv_contains (locales, locale1) ||
145 		       g_strv_contains (locales, lang1);
146 	}
147 
148 	/* both specified */
149 	if (g_strcmp0 (locale1, locale2) == 0)
150 		return TRUE;
151 	if (g_strcmp0 (locale1, lang2) == 0)
152 		return TRUE;
153 	if (g_strcmp0 (lang1, locale2) == 0)
154 		return TRUE;
155 	return FALSE;
156 }
157 
158 /**
159  * as_utils_is_stock_icon_name:
160  * @name: an icon name
161  *
162  * Searches the known list of stock icons.
163  *
164  * Returns: %TRUE if the icon is a "stock icon name" and does not need to be
165  *          included in the AppStream icon tarball
166  *
167  * Since: 0.1.3
168  **/
169 gboolean
as_utils_is_stock_icon_name(const gchar * name)170 as_utils_is_stock_icon_name (const gchar *name)
171 {
172 	g_autoptr(GBytes) data = NULL;
173 	g_autofree gchar *key = NULL;
174 	gchar *tmp;
175 
176 	/* load the readonly data section and look for the icon name */
177 	data = g_resource_lookup_data (as_get_resource (),
178 				       "/org/freedesktop/appstream-glib/as-stock-icons.txt",
179 				       G_RESOURCE_LOOKUP_FLAGS_NONE,
180 				       NULL);
181 	if (data == NULL)
182 		return FALSE;
183 	key = g_strdup_printf ("\n%s\n", name);
184 	tmp = g_strstr_len (key, -1, "-symbolic");
185 	if (tmp != NULL) {
186 		tmp[0] = '\n';
187 		tmp[1] = '\0';
188 	}
189 	return g_strstr_len (g_bytes_get_data (data, NULL), -1, key) != NULL;
190 }
191 
192 /**
193  * as_utils_is_spdx_license_id:
194  * @license_id: a single SPDX license ID, e.g. "CC-BY-3.0"
195  *
196  * Searches the known list of SPDX license IDs.
197  *
198  * Returns: %TRUE if the license ID is a valid "SPDX license ID"
199  *
200  * Since: 0.1.5
201  **/
202 gboolean
as_utils_is_spdx_license_id(const gchar * license_id)203 as_utils_is_spdx_license_id (const gchar *license_id)
204 {
205 	g_autoptr(GBytes) data = NULL;
206 	g_autofree gchar *key = NULL;
207 
208 	/* handle invalid */
209 	if (license_id == NULL || license_id[0] == '\0')
210 		return FALSE;
211 
212 	/* this is used to map non-SPDX licence-ids to legitimate values */
213 	if (g_str_has_prefix (license_id, "LicenseRef-"))
214 		return TRUE;
215 
216 	/* load the readonly data section and look for the license ID */
217 	data = g_resource_lookup_data (as_get_resource (),
218 				       "/org/freedesktop/appstream-glib/as-license-ids.txt",
219 				       G_RESOURCE_LOOKUP_FLAGS_NONE,
220 				       NULL);
221 	if (data == NULL)
222 		return FALSE;
223 	key = g_strdup_printf ("\n%s\n", license_id);
224 	return g_strstr_len (g_bytes_get_data (data, NULL), -1, key) != NULL;
225 }
226 
227 /**
228  * as_utils_is_blacklisted_id:
229  * @desktop_id: a desktop ID, e.g. "gimp.desktop"
230  *
231  * Searches the known list of blacklisted desktop IDs.
232  *
233  * Returns: %TRUE if the desktop ID is blacklisted
234  *
235  * Since: 0.2.2
236  **/
237 gboolean
as_utils_is_blacklisted_id(const gchar * desktop_id)238 as_utils_is_blacklisted_id (const gchar *desktop_id)
239 {
240 	return FALSE;
241 }
242 
243 /**
244  * as_utils_is_environment_id:
245  * @environment_id: a desktop ID, e.g. "GNOME"
246  *
247  * Searches the known list of registered environment IDs.
248  *
249  * Returns: %TRUE if the environment ID is valid
250  *
251  * Since: 0.2.4
252  **/
253 gboolean
as_utils_is_environment_id(const gchar * environment_id)254 as_utils_is_environment_id (const gchar *environment_id)
255 {
256 	g_autoptr(GBytes) data = NULL;
257 	g_autofree gchar *key = NULL;
258 
259 	/* load the readonly data section and look for the environment ID */
260 	data = g_resource_lookup_data (as_get_resource (),
261 				       "/org/freedesktop/appstream-glib/as-environment-ids.txt",
262 				       G_RESOURCE_LOOKUP_FLAGS_NONE,
263 				       NULL);
264 	if (data == NULL)
265 		return FALSE;
266 	key = g_strdup_printf ("\n%s\n", environment_id);
267 	return g_strstr_len (g_bytes_get_data (data, NULL), -1, key) != NULL;
268 }
269 
270 /**
271  * as_utils_is_category_id:
272  * @category_id: a desktop ID, e.g. "AudioVideoEditing"
273  *
274  * Searches the known list of registered category IDs.
275  *
276  * Returns: %TRUE if the category ID is valid
277  *
278  * Since: 0.2.4
279  **/
280 gboolean
as_utils_is_category_id(const gchar * category_id)281 as_utils_is_category_id (const gchar *category_id)
282 {
283 	g_autoptr(GBytes) data = NULL;
284 	g_autofree gchar *key = NULL;
285 
286 	/* load the readonly data section and look for the category ID */
287 	data = g_resource_lookup_data (as_get_resource (),
288 				       "/org/freedesktop/appstream-glib/as-category-ids.txt",
289 				       G_RESOURCE_LOOKUP_FLAGS_NONE,
290 				       NULL);
291 	if (data == NULL)
292 		return FALSE;
293 	key = g_strdup_printf ("\n%s\n", category_id);
294 	return g_strstr_len (g_bytes_get_data (data, NULL), -1, key) != NULL;
295 }
296 
297 typedef struct {
298 	gboolean	 last_token_literal;
299 	GPtrArray	*array;
300 	GString		*collect;
301 } AsUtilsSpdxHelper;
302 
303 static gpointer
_g_ptr_array_last(GPtrArray * array)304 _g_ptr_array_last (GPtrArray *array)
305 {
306 	return g_ptr_array_index (array, array->len - 1);
307 }
308 
309 static void
as_utils_spdx_license_tokenize_drop(AsUtilsSpdxHelper * helper)310 as_utils_spdx_license_tokenize_drop (AsUtilsSpdxHelper *helper)
311 {
312 	const gchar *tmp = helper->collect->str;
313 	guint i;
314 	g_autofree gchar *last_literal = NULL;
315 	struct {
316 		const gchar	*old;
317 		const gchar	*new;
318 	} licenses[] =  {
319 		{ "CC0",	"CC0-1.0" },
320 		{ "CC-BY",	"CC-BY-3.0" },
321 		{ "CC-BY-SA",	"CC-BY-SA-3.0" },
322 		{ "GFDL",	"GFDL-1.3" },
323 		{ "GPL-2",	"GPL-2.0" },
324 		{ "GPL-3",	"GPL-3.0" },
325 		{ "proprietary", "LicenseRef-proprietary" },
326 		{ NULL, NULL } };
327 
328 	/* nothing from last time */
329 	if (helper->collect->len == 0)
330 		return;
331 
332 	/* is license enum */
333 	if (as_utils_is_spdx_license_id (tmp)) {
334 		g_ptr_array_add (helper->array, g_strdup_printf ("@%s", tmp));
335 		helper->last_token_literal = FALSE;
336 		g_string_truncate (helper->collect, 0);
337 		return;
338 	}
339 
340 	/* is license enum with "+" */
341 	if (g_str_has_suffix (tmp, "+")) {
342 		g_autofree gchar *license_id = g_strndup (tmp, strlen (tmp) - 1);
343 		if (as_utils_is_spdx_license_id (license_id)) {
344 			g_ptr_array_add (helper->array, g_strdup_printf ("@%s", license_id));
345 			g_ptr_array_add (helper->array, g_strdup ("+"));
346 			helper->last_token_literal = FALSE;
347 			g_string_truncate (helper->collect, 0);
348 			return;
349 		}
350 	}
351 
352 	/* is old license enum */
353 	for (i = 0; licenses[i].old != NULL; i++) {
354 		if (g_strcmp0 (tmp, licenses[i].old) != 0)
355 			continue;
356 		g_ptr_array_add (helper->array,
357 				 g_strdup_printf ("@%s", licenses[i].new));
358 		helper->last_token_literal = FALSE;
359 		g_string_truncate (helper->collect, 0);
360 		return;
361 	}
362 
363 	/* is conjunctive */
364 	if (g_strcmp0 (tmp, "and") == 0 || g_strcmp0 (tmp, "AND") == 0) {
365 		g_ptr_array_add (helper->array, g_strdup ("&"));
366 		helper->last_token_literal = FALSE;
367 		g_string_truncate (helper->collect, 0);
368 		return;
369 	}
370 
371 	/* is disjunctive */
372 	if (g_strcmp0 (tmp, "or") == 0 || g_strcmp0 (tmp, "OR") == 0) {
373 		g_ptr_array_add (helper->array, g_strdup ("|"));
374 		helper->last_token_literal = FALSE;
375 		g_string_truncate (helper->collect, 0);
376 		return;
377 	}
378 
379 	/* is literal */
380 	if (helper->last_token_literal) {
381 		last_literal = g_strdup (_g_ptr_array_last (helper->array));
382 		g_ptr_array_remove_index (helper->array, helper->array->len - 1);
383 		g_ptr_array_add (helper->array,
384 				 g_strdup_printf ("%s %s", last_literal, tmp));
385 	} else {
386 		g_ptr_array_add (helper->array, g_strdup (tmp));
387 		helper->last_token_literal = TRUE;
388 	}
389 	g_string_truncate (helper->collect, 0);
390 }
391 
392 /* SPDX decided to rename some of the really common license IDs in v3
393  * which broke a lot of tools that we cannot really fix now */
394 static GString *
as_utils_spdx_license_3to2(const gchar * license3)395 as_utils_spdx_license_3to2 (const gchar *license3)
396 {
397 	GString *license2 = g_string_new (license3);
398 	as_utils_string_replace (license2, "-only", "");
399 	as_utils_string_replace (license2, "-or-later", "+");
400 	return license2;
401 }
402 
403 /**
404  * as_utils_spdx_license_tokenize:
405  * @license: a license string, e.g. "LGPLv2+ and (QPL or GPLv2) and MIT"
406  *
407  * Tokenizes the SPDX license string (or any simarly formatted string)
408  * into parts. Any licence parts of the string e.g. "LGPL-2.0+" are prefexed
409  * with "@", the conjunctive replaced with "&" and the disjunctive replaced
410  * with "|". Brackets are added as indervidual tokens and other strings are
411  * appended into single tokens where possible.
412  *
413  * Returns: (transfer full): array of strings, or %NULL for invalid
414  *
415  * Since: 0.1.5
416  **/
417 gchar **
as_utils_spdx_license_tokenize(const gchar * license)418 as_utils_spdx_license_tokenize (const gchar *license)
419 {
420 	AsUtilsSpdxHelper helper;
421 	g_autoptr(GString) license2 = NULL;
422 
423 	/* handle invalid */
424 	if (license == NULL)
425 		return NULL;
426 
427 	/* SPDX broke the world with v3 */
428 	license2 = as_utils_spdx_license_3to2 (license);
429 
430 	helper.last_token_literal = FALSE;
431 	helper.collect = g_string_new ("");
432 	helper.array = g_ptr_array_new_with_free_func (g_free);
433 	for (guint i = 0; i < license2->len; i++) {
434 
435 		/* handle brackets */
436 		const gchar tmp = license2->str[i];
437 		if (tmp == '(' || tmp == ')') {
438 			as_utils_spdx_license_tokenize_drop (&helper);
439 			g_ptr_array_add (helper.array, g_strdup_printf ("%c", tmp));
440 			helper.last_token_literal = FALSE;
441 			continue;
442 		}
443 
444 		/* space, so dump queue */
445 		if (tmp == ' ') {
446 			as_utils_spdx_license_tokenize_drop (&helper);
447 			continue;
448 		}
449 		g_string_append_c (helper.collect, tmp);
450 	}
451 
452 	/* dump anything remaining */
453 	as_utils_spdx_license_tokenize_drop (&helper);
454 
455 	/* return GStrv */
456 	g_ptr_array_add (helper.array, NULL);
457 	g_string_free (helper.collect, TRUE);
458 	return (gchar **) g_ptr_array_free (helper.array, FALSE);
459 }
460 
461 /**
462  * as_utils_spdx_license_detokenize:
463  * @license_tokens: license tokens, typically from as_utils_spdx_license_tokenize()
464  *
465  * De-tokenizes the SPDX licenses into a string.
466  *
467  * Returns: (transfer full): string, or %NULL for invalid
468  *
469  * Since: 0.2.5
470  **/
471 gchar *
as_utils_spdx_license_detokenize(gchar ** license_tokens)472 as_utils_spdx_license_detokenize (gchar **license_tokens)
473 {
474 	GString *tmp;
475 	guint i;
476 
477 	/* handle invalid */
478 	if (license_tokens == NULL)
479 		return NULL;
480 
481 	tmp = g_string_new ("");
482 	for (i = 0; license_tokens[i] != NULL; i++) {
483 		if (g_strcmp0 (license_tokens[i], "&") == 0) {
484 			g_string_append (tmp, " AND ");
485 			continue;
486 		}
487 		if (g_strcmp0 (license_tokens[i], "|") == 0) {
488 			g_string_append (tmp, " OR ");
489 			continue;
490 		}
491 		if (g_strcmp0 (license_tokens[i], "+") == 0) {
492 			g_string_append (tmp, "+");
493 			continue;
494 		}
495 		if (license_tokens[i][0] != '@') {
496 			g_string_append (tmp, license_tokens[i]);
497 			continue;
498 		}
499 		g_string_append (tmp, license_tokens[i] + 1);
500 	}
501 	return g_string_free (tmp, FALSE);
502 }
503 
504 /**
505  * as_utils_is_spdx_license:
506  * @license: a SPDX license string, e.g. "CC-BY-3.0 and GFDL-1.3"
507  *
508  * Checks the licence string to check it being a valid licence.
509  * NOTE: SPDX licences can't typically contain brackets.
510  *
511  * Returns: %TRUE if the license is a valid "SPDX license"
512  *
513  * Since: 0.2.5
514  **/
515 gboolean
as_utils_is_spdx_license(const gchar * license)516 as_utils_is_spdx_license (const gchar *license)
517 {
518 	guint i;
519 	g_auto(GStrv) tokens = NULL;
520 
521 	/* handle nothing set */
522 	if (license == NULL || license[0] == '\0')
523 		return FALSE;
524 
525 	/* no license information whatsoever */
526 	if (g_strcmp0 (license, "NONE") == 0)
527 		return TRUE;
528 
529 	/* creator has intentionally provided no information */
530 	if (g_strcmp0 (license, "NOASSERTION") == 0)
531 		return TRUE;
532 
533 	tokens = as_utils_spdx_license_tokenize (license);
534 	if (tokens == NULL)
535 		return FALSE;
536 	for (i = 0; tokens[i] != NULL; i++) {
537 		if (tokens[i][0] == '@') {
538 			if (as_utils_is_spdx_license_id (tokens[i] + 1))
539 				continue;
540 		}
541 		if (as_utils_is_spdx_license_id (tokens[i]))
542 			continue;
543 		if (g_strcmp0 (tokens[i], "&") == 0)
544 			continue;
545 		if (g_strcmp0 (tokens[i], "|") == 0)
546 			continue;
547 		if (g_strcmp0 (tokens[i], "+") == 0)
548 			continue;
549 		return FALSE;
550 	}
551 	return TRUE;
552 }
553 
554 /**
555  * as_utils_license_to_spdx:
556  * @license: a not-quite SPDX license string, e.g. "GPLv3+"
557  *
558  * Converts a non-SPDX license into an SPDX format string where possible.
559  *
560  * Returns: the best-effort SPDX license string
561  *
562  * Since: 0.5.5
563  **/
564 gchar *
as_utils_license_to_spdx(const gchar * license)565 as_utils_license_to_spdx (const gchar *license)
566 {
567 	GString *str;
568 	guint i;
569 	guint j;
570 	guint license_len;
571 	struct {
572 		const gchar	*old;
573 		const gchar	*new;
574 	} convert[] =  {
575 		{ " with exceptions",		NULL },
576 		{ " with advertising",		NULL },
577 		{ " and ",			" AND " },
578 		{ " or ",			" OR " },
579 		{ "AGPLv3+",			"AGPL-3.0" },
580 		{ "AGPLv3",			"AGPL-3.0" },
581 		{ "Artistic 2.0",		"Artistic-2.0" },
582 		{ "Artistic clarified",		"Artistic-2.0" },
583 		{ "Artistic",			"Artistic-1.0" },
584 		{ "ASL 1.1",			"Apache-1.1" },
585 		{ "ASL 2.0",			"Apache-2.0" },
586 		{ "Boost",			"BSL-1.0" },
587 		{ "BSD",			"BSD-3-Clause" },
588 		{ "CC0",			"CC0-1.0" },
589 		{ "CC-BY-SA",			"CC-BY-SA-3.0" },
590 		{ "CC-BY",			"CC-BY-3.0" },
591 		{ "CDDL",			"CDDL-1.0" },
592 		{ "CeCILL-C",			"CECILL-C" },
593 		{ "CeCILL",			"CECILL-2.0" },
594 		{ "CPAL",			"CPAL-1.0" },
595 		{ "CPL",			"CPL-1.0" },
596 		{ "EPL",			"EPL-1.0" },
597 		{ "Free Art",			"ClArtistic" },
598 		{ "GFDL",			"GFDL-1.3" },
599 		{ "GPL+",			"GPL-1.0+" },
600 		{ "GPLv2+",			"GPL-2.0+" },
601 		{ "GPLv2",			"GPL-2.0" },
602 		{ "GPLv3+",			"GPL-3.0+" },
603 		{ "GPLv3",			"GPL-3.0" },
604 		{ "IBM",			"IPL-1.0" },
605 		{ "LGPL+",			"LGPL-2.1+" },
606 		{ "LGPLv2.1",			"LGPL-2.1" },
607 		{ "LGPLv2+",			"LGPL-2.1+" },
608 		{ "LGPLv2",			"LGPL-2.1" },
609 		{ "LGPLv3+",			"LGPL-3.0+" },
610 		{ "LGPLv3",			"LGPL-3.0" },
611 		{ "LPPL",			"LPPL-1.3c" },
612 		{ "MPLv1.0",			"MPL-1.0" },
613 		{ "MPLv1.1",			"MPL-1.1" },
614 		{ "MPLv2.0",			"MPL-2.0" },
615 		{ "Netscape",			"NPL-1.1" },
616 		{ "OFL",			"OFL-1.1" },
617 		{ "Python",			"Python-2.0" },
618 		{ "QPL",			"QPL-1.0" },
619 		{ "SPL",			"SPL-1.0" },
620 		{ "UPL",			"UPL-1.0" },
621 		{ "zlib",			"Zlib" },
622 		{ "ZPLv2.0",			"ZPL-2.0" },
623 		{ "Unlicense",			"CC0-1.0" },
624 		{ "Public Domain",		"LicenseRef-public-domain" },
625 		{ "SUSE-Public-Domain",		"LicenseRef-public-domain" },
626 		{ "Copyright only",		"LicenseRef-public-domain" },
627 		{ "Proprietary",		"LicenseRef-proprietary" },
628 		{ "Commercial",			"LicenseRef-proprietary" },
629 		{ NULL, NULL } };
630 
631 	/* nothing set */
632 	if (license == NULL)
633 		return NULL;
634 
635 	/* already in SPDX format */
636 	if (as_utils_is_spdx_license (license))
637 		return g_strdup (license);
638 
639 	/* go through the string looking for case-insensitive matches */
640 	str = g_string_new ("");
641 	license_len = (guint) strlen (license);
642 	for (i = 0; i < license_len; i++) {
643 		gboolean found = FALSE;
644 		for (j = 0; convert[j].old != NULL; j++) {
645 			guint old_len = (guint) strlen (convert[j].old);
646 			if (g_ascii_strncasecmp (license + i,
647 						 convert[j].old,
648 						 old_len) != 0)
649 				continue;
650 			if (convert[j].new != NULL)
651 				g_string_append (str, convert[j].new);
652 			i += old_len - 1;
653 			found = TRUE;
654 		}
655 		if (!found)
656 			g_string_append_c (str, license[i]);
657 	}
658 	return g_string_free (str, FALSE);
659 }
660 
661 static void
as_pixbuf_blur_private(GdkPixbuf * src,GdkPixbuf * dest,gint radius,guchar * div_kernel_size)662 as_pixbuf_blur_private (GdkPixbuf *src, GdkPixbuf *dest, gint radius, guchar *div_kernel_size)
663 {
664 	gint width, height, src_rowstride, dest_rowstride, n_channels;
665 	guchar *p_src, *p_dest, *c1, *c2;
666 	gint x, y, i, i1, i2, width_minus_1, height_minus_1, radius_plus_1;
667 	gint r, g, b, a;
668 	guchar *p_dest_row, *p_dest_col;
669 
670 	width = gdk_pixbuf_get_width (src);
671 	height = gdk_pixbuf_get_height (src);
672 	n_channels = gdk_pixbuf_get_n_channels (src);
673 	radius_plus_1 = radius + 1;
674 
675 	/* horizontal blur */
676 	p_src = gdk_pixbuf_get_pixels (src);
677 	p_dest = gdk_pixbuf_get_pixels (dest);
678 	src_rowstride = gdk_pixbuf_get_rowstride (src);
679 	dest_rowstride = gdk_pixbuf_get_rowstride (dest);
680 	width_minus_1 = width - 1;
681 	for (y = 0; y < height; y++) {
682 
683 		/* calc the initial sums of the kernel */
684 		r = g = b = a = 0;
685 		for (i = -radius; i <= radius; i++) {
686 			c1 = p_src + (CLAMP (i, 0, width_minus_1) * n_channels);
687 			r += c1[0];
688 			g += c1[1];
689 			b += c1[2];
690 		}
691 
692 		p_dest_row = p_dest;
693 		for (x = 0; x < width; x++) {
694 			/* set as the mean of the kernel */
695 			p_dest_row[0] = div_kernel_size[r];
696 			p_dest_row[1] = div_kernel_size[g];
697 			p_dest_row[2] = div_kernel_size[b];
698 			p_dest_row += n_channels;
699 
700 			/* the pixel to add to the kernel */
701 			i1 = x + radius_plus_1;
702 			if (i1 > width_minus_1)
703 				i1 = width_minus_1;
704 			c1 = p_src + (i1 * n_channels);
705 
706 			/* the pixel to remove from the kernel */
707 			i2 = x - radius;
708 			if (i2 < 0)
709 				i2 = 0;
710 			c2 = p_src + (i2 * n_channels);
711 
712 			/* calc the new sums of the kernel */
713 			r += c1[0] - c2[0];
714 			g += c1[1] - c2[1];
715 			b += c1[2] - c2[2];
716 		}
717 
718 		p_src += src_rowstride;
719 		p_dest += dest_rowstride;
720 	}
721 
722 	/* vertical blur */
723 	p_src = gdk_pixbuf_get_pixels (dest);
724 	p_dest = gdk_pixbuf_get_pixels (src);
725 	src_rowstride = gdk_pixbuf_get_rowstride (dest);
726 	dest_rowstride = gdk_pixbuf_get_rowstride (src);
727 	height_minus_1 = height - 1;
728 	for (x = 0; x < width; x++) {
729 
730 		/* calc the initial sums of the kernel */
731 		r = g = b = a = 0;
732 		for (i = -radius; i <= radius; i++) {
733 			c1 = p_src + (CLAMP (i, 0, height_minus_1) * src_rowstride);
734 			r += c1[0];
735 			g += c1[1];
736 			b += c1[2];
737 		}
738 
739 		p_dest_col = p_dest;
740 		for (y = 0; y < height; y++) {
741 			/* set as the mean of the kernel */
742 
743 			p_dest_col[0] = div_kernel_size[r];
744 			p_dest_col[1] = div_kernel_size[g];
745 			p_dest_col[2] = div_kernel_size[b];
746 			p_dest_col += dest_rowstride;
747 
748 			/* the pixel to add to the kernel */
749 			i1 = y + radius_plus_1;
750 			if (i1 > height_minus_1)
751 				i1 = height_minus_1;
752 			c1 = p_src + (i1 * src_rowstride);
753 
754 			/* the pixel to remove from the kernel */
755 			i2 = y - radius;
756 			if (i2 < 0)
757 				i2 = 0;
758 			c2 = p_src + (i2 * src_rowstride);
759 
760 			/* calc the new sums of the kernel */
761 			r += c1[0] - c2[0];
762 			g += c1[1] - c2[1];
763 			b += c1[2] - c2[2];
764 		}
765 
766 		p_src += n_channels;
767 		p_dest += n_channels;
768 	}
769 }
770 
771 /**
772  * as_pixbuf_blur:
773  * @src: the GdkPixbuf.
774  * @radius: the pixel radius for the gaussian blur, typical values are 1..3
775  * @iterations: Amount to blur the image, typical values are 1..5
776  *
777  * Blurs an image. Warning, this method is s..l..o..w... for large images.
778  *
779  * Since: 0.3.2
780  **/
781 void
as_pixbuf_blur(GdkPixbuf * src,gint radius,gint iterations)782 as_pixbuf_blur (GdkPixbuf *src, gint radius, gint iterations)
783 {
784 	gint kernel_size;
785 	gint i;
786 	g_autofree guchar *div_kernel_size = NULL;
787 	g_autoptr(GdkPixbuf) tmp = NULL;
788 
789 	tmp = gdk_pixbuf_new (gdk_pixbuf_get_colorspace (src),
790 			      gdk_pixbuf_get_has_alpha (src),
791 			      gdk_pixbuf_get_bits_per_sample (src),
792 			      gdk_pixbuf_get_width (src),
793 			      gdk_pixbuf_get_height (src));
794 	kernel_size = 2 * radius + 1;
795 	div_kernel_size = g_new (guchar, 256 * kernel_size);
796 	for (i = 0; i < 256 * kernel_size; i++)
797 		div_kernel_size[i] = (guchar) (i / kernel_size);
798 
799 	while (iterations-- > 0)
800 		as_pixbuf_blur_private (src, tmp, radius, div_kernel_size);
801 }
802 
803 #define interpolate_value(original, reference, distance)		\
804 	(CLAMP (((distance) * (reference)) +				\
805 		((1.0 - (distance)) * (original)), 0, 255))
806 
807 /**
808  * as_pixbuf_sharpen: (skip)
809  * @src: the GdkPixbuf.
810  * @radius: the pixel radius for the unsharp mask, typical values are 1..3
811  * @amount: Amount to sharpen the image, typical values are -0.1 to -0.9
812  *
813  * Sharpens an image. Warning, this method is s..l..o..w... for large images.
814  *
815  * Since: 0.2.2
816  **/
817 void
as_pixbuf_sharpen(GdkPixbuf * src,gint radius,gdouble amount)818 as_pixbuf_sharpen (GdkPixbuf *src, gint radius, gdouble amount)
819 {
820 	gint width, height, rowstride, n_channels;
821 	gint x, y;
822 	guchar *p_blurred;
823 	guchar *p_blurred_row;
824 	guchar *p_src;
825 	guchar *p_src_row;
826 	g_autoptr(GdkPixbuf) blurred = NULL;
827 
828 	blurred = gdk_pixbuf_copy (src);
829 	as_pixbuf_blur (blurred, radius, 3);
830 
831 	width = gdk_pixbuf_get_width (src);
832 	height = gdk_pixbuf_get_height (src);
833 	rowstride = gdk_pixbuf_get_rowstride (src);
834 	n_channels = gdk_pixbuf_get_n_channels (src);
835 
836 	p_src = gdk_pixbuf_get_pixels (src);
837 	p_blurred = gdk_pixbuf_get_pixels (blurred);
838 
839 	for (y = 0; y < height; y++) {
840 		p_src_row = p_src;
841 		p_blurred_row = p_blurred;
842 		for (x = 0; x < width; x++) {
843 			p_src_row[0] = (guchar) interpolate_value (p_src_row[0],
844 							  p_blurred_row[0],
845 							  amount);
846 			p_src_row[1] = (guchar) interpolate_value (p_src_row[1],
847 							  p_blurred_row[1],
848 							  amount);
849 			p_src_row[2] = (guchar) interpolate_value (p_src_row[2],
850 							  p_blurred_row[2],
851 							  amount);
852 			p_src_row += n_channels;
853 			p_blurred_row += n_channels;
854 		}
855 		p_src += rowstride;
856 		p_blurred += rowstride;
857 	}
858 }
859 
860 /**
861  * as_utils_find_icon_filename_full:
862  * @destdir: the destdir.
863  * @search: the icon search name, e.g. "microphone.svg"
864  * @flags: A #AsUtilsFindIconFlag bitfield
865  * @error: A #GError or %NULL
866  *
867  * Finds an icon filename from a filesystem root.
868  *
869  * Returns: (transfer full): a newly allocated %NULL terminated string
870  *
871  * Since: 0.3.1
872  **/
873 gchar *
as_utils_find_icon_filename_full(const gchar * destdir,const gchar * search,AsUtilsFindIconFlag flags,GError ** error)874 as_utils_find_icon_filename_full (const gchar *destdir,
875 				  const gchar *search,
876 				  AsUtilsFindIconFlag flags,
877 				  GError **error)
878 {
879 	guint i;
880 	guint j;
881 	guint k;
882 	guint m;
883 	const gchar **sizes;
884 	const gchar *pixmap_dirs[] = { "pixmaps", "icons", NULL };
885 	const gchar *theme_dirs[] = { "hicolor", "oxygen", NULL };
886 	const gchar *supported_ext[] = { ".png",
887 					 ".gif",
888 					 ".svg",
889 					 ".xpm",
890 					 "",
891 					 NULL };
892 	const gchar *sizes_lo_dpi[] = { "64x64",
893 					"128x128",
894 					"96x96",
895 					"256x256",
896 					"512x512",
897 					"scalable",
898 					"48x48",
899 					"32x32",
900 					"24x24",
901 					"16x16",
902 					NULL };
903 	const gchar *sizes_hi_dpi[] = { "128x128",
904 					"256x256",
905 					"512x512",
906 					"scalable",
907 					NULL };
908 	const gchar *types[] = { "actions",
909 				 "animations",
910 				 "apps",
911 				 "categories",
912 				 "devices",
913 				 "emblems",
914 				 "emotes",
915 				 "filesystems",
916 				 "intl",
917 				 "mimetypes",
918 				 "places",
919 				 "status",
920 				 "stock",
921 				 NULL };
922 	g_autofree gchar *prefix = NULL;
923 
924 	g_return_val_if_fail (search != NULL, NULL);
925 
926 	/* fallback */
927 	if (destdir == NULL)
928 		destdir = "";
929 
930 	/* is this an absolute path */
931 	if (search[0] == '/') {
932 		g_autofree gchar *tmp = NULL;
933 		tmp = g_build_filename (destdir, search, NULL);
934 		if (!g_file_test (tmp, G_FILE_TEST_EXISTS)) {
935 			g_set_error (error,
936 				     AS_UTILS_ERROR,
937 				     AS_UTILS_ERROR_FAILED,
938 				     "specified icon '%s' does not exist",
939 				     search);
940 			return NULL;
941 		}
942 		return g_strdup (tmp);
943 	}
944 
945 	/* all now found in the prefix */
946 	prefix = g_strdup_printf ("%s/usr", destdir);
947 	if (!g_file_test (prefix, G_FILE_TEST_EXISTS)) {
948 		g_free (prefix);
949 		prefix = g_strdup (destdir);
950 	}
951 	if (!g_file_test (prefix, G_FILE_TEST_EXISTS)) {
952 		g_set_error (error,
953 			     AS_UTILS_ERROR,
954 			     AS_UTILS_ERROR_FAILED,
955 			     "Failed to find icon in prefix %s", search);
956 		return NULL;
957 	}
958 
959 	/* icon theme apps */
960 	sizes = flags & AS_UTILS_FIND_ICON_HI_DPI ? sizes_hi_dpi : sizes_lo_dpi;
961 	for (k = 0; theme_dirs[k] != NULL; k++) {
962 		for (i = 0; sizes[i] != NULL; i++) {
963 			for (m = 0; types[m] != NULL; m++) {
964 				for (j = 0; supported_ext[j] != NULL; j++) {
965 					g_autofree gchar *tmp = NULL;
966 					tmp = g_strdup_printf ("%s/share/icons/"
967 							       "%s/%s/%s/%s%s",
968 							       prefix,
969 							       theme_dirs[k],
970 							       sizes[i],
971 							       types[m],
972 							       search,
973 							       supported_ext[j]);
974 					if (g_file_test (tmp, G_FILE_TEST_EXISTS))
975 						return g_strdup (tmp);
976 				}
977 			}
978 		}
979 	}
980 
981 	/* pixmap */
982 	for (i = 0; pixmap_dirs[i] != NULL; i++) {
983 		for (j = 0; supported_ext[j] != NULL; j++) {
984 			g_autofree gchar *tmp = NULL;
985 			tmp = g_strdup_printf ("%s/share/%s/%s%s",
986 					       prefix,
987 					       pixmap_dirs[i],
988 					       search,
989 					       supported_ext[j]);
990 			if (g_file_test (tmp, G_FILE_TEST_IS_REGULAR))
991 				return g_strdup (tmp);
992 		}
993 	}
994 
995 	/* failed */
996 	g_set_error (error,
997 		     AS_UTILS_ERROR,
998 		     AS_UTILS_ERROR_FAILED,
999 		     "Failed to find icon %s", search);
1000 	return NULL;
1001 }
1002 
1003 /**
1004  * as_utils_find_icon_filename:
1005  * @destdir: the destdir.
1006  * @search: the icon search name, e.g. "microphone.svg"
1007  * @error: A #GError or %NULL
1008  *
1009  * Finds an icon filename from a filesystem root.
1010  *
1011  * Returns: (transfer full): a newly allocated %NULL terminated string
1012  *
1013  * Since: 0.2.5
1014  **/
1015 gchar *
as_utils_find_icon_filename(const gchar * destdir,const gchar * search,GError ** error)1016 as_utils_find_icon_filename (const gchar *destdir,
1017 			     const gchar *search,
1018 			     GError **error)
1019 {
1020 	return as_utils_find_icon_filename_full (destdir, search,
1021 						 AS_UTILS_FIND_ICON_NONE,
1022 						 error);
1023 }
1024 
1025 static const gchar *
as_utils_location_get_prefix(AsUtilsLocation location)1026 as_utils_location_get_prefix (AsUtilsLocation location)
1027 {
1028 	if (location == AS_UTILS_LOCATION_SHARED)
1029 		return "/usr/share";
1030 	if (location == AS_UTILS_LOCATION_CACHE)
1031 		return "/var/cache";
1032 	if (location == AS_UTILS_LOCATION_USER)
1033 		return "~/.local/share";
1034 	return NULL;
1035 }
1036 
1037 static gboolean
as_utils_install_icon(AsUtilsLocation location,const gchar * filename,const gchar * origin,const gchar * destdir,GError ** error)1038 as_utils_install_icon (AsUtilsLocation location,
1039 		       const gchar *filename,
1040 		       const gchar *origin,
1041 		       const gchar *destdir,
1042 		       GError **error)
1043 {
1044 	const gchar *pathname;
1045 	const gchar *tmp;
1046 	gboolean ret = TRUE;
1047 	gsize len;
1048 	int r;
1049 	struct archive *arch = NULL;
1050 	struct archive_entry *entry;
1051 	g_autofree gchar *data = NULL;
1052 	g_autofree gchar *dir = NULL;
1053 
1054 	dir = g_strdup_printf ("%s%s/app-info/icons/%s",
1055 			       destdir,
1056 			       as_utils_location_get_prefix (location),
1057 			       origin);
1058 
1059 	/* load file at once to avoid seeking */
1060 	ret = g_file_get_contents (filename, &data, &len, error);
1061 	if (!ret)
1062 		goto out;
1063 
1064 	/* read anything */
1065 	arch = archive_read_new ();
1066 	archive_read_support_format_all (arch);
1067 	archive_read_support_filter_all (arch);
1068 	r = archive_read_open_memory (arch, data, len);
1069 	if (r) {
1070 		ret = FALSE;
1071 		g_set_error (error,
1072 			     AS_UTILS_ERROR,
1073 			     AS_UTILS_ERROR_FAILED,
1074 			     "Cannot open %s: %s",
1075 			     filename,
1076 			     archive_error_string (arch));
1077 		goto out;
1078 	}
1079 
1080 	/* decompress each file */
1081 	for (;;) {
1082 		g_autofree gchar *buf = NULL;
1083 
1084 		r = archive_read_next_header (arch, &entry);
1085 		if (r == ARCHIVE_EOF)
1086 			break;
1087 		if (r != ARCHIVE_OK) {
1088 			ret = FALSE;
1089 			g_set_error (error,
1090 				     AS_UTILS_ERROR,
1091 				     AS_UTILS_ERROR_FAILED,
1092 				     "Cannot read header: %s",
1093 				     archive_error_string (arch));
1094 			goto out;
1095 		}
1096 
1097 		/* no output file */
1098 		pathname = archive_entry_pathname (entry);
1099 		if (pathname == NULL)
1100 			continue;
1101 
1102 		/* update output path */
1103 		buf = g_build_filename (dir, pathname, NULL);
1104 		archive_entry_update_pathname_utf8 (entry, buf);
1105 
1106 		/* update hardlinks */
1107 		tmp = archive_entry_hardlink (entry);
1108 		if (tmp != NULL) {
1109 			g_autofree gchar *buf_link = NULL;
1110 			buf_link = g_build_filename (dir, tmp, NULL);
1111 			archive_entry_update_hardlink_utf8 (entry, buf_link);
1112 		}
1113 
1114 		/* update symlinks */
1115 		tmp = archive_entry_symlink (entry);
1116 		if (tmp != NULL) {
1117 			g_autofree gchar *buf_link = NULL;
1118 			buf_link = g_build_filename (dir, tmp, NULL);
1119 			archive_entry_update_symlink_utf8 (entry, buf_link);
1120 		}
1121 
1122 		r = archive_read_extract (arch, entry, 0);
1123 		if (r != ARCHIVE_OK) {
1124 			ret = FALSE;
1125 			g_set_error (error,
1126 				     AS_UTILS_ERROR,
1127 				     AS_UTILS_ERROR_FAILED,
1128 				     "Cannot extract: %s",
1129 				     archive_error_string (arch));
1130 			goto out;
1131 		}
1132 	}
1133 out:
1134 	if (arch != NULL) {
1135 		archive_read_close (arch);
1136 		archive_read_free (arch);
1137 	}
1138 	return ret;
1139 }
1140 
1141 static gboolean
as_utils_install_xml(const gchar * filename,const gchar * origin,const gchar * dir,const gchar * destdir,GError ** error)1142 as_utils_install_xml (const gchar *filename,
1143 		      const gchar *origin,
1144 		      const gchar *dir,
1145 		      const gchar *destdir,
1146 		      GError **error)
1147 {
1148 	gchar *tmp;
1149 	g_autofree gchar *basename = NULL;
1150 	g_autofree gchar *path_dest = NULL;
1151 	g_autofree gchar *path_parent = NULL;
1152 	g_autoptr(GFile) file_dest = NULL;
1153 	g_autoptr(GFile) file_src = NULL;
1154 
1155 	/* create directory structure */
1156 	path_parent = g_strdup_printf ("%s%s", destdir, dir);
1157 	if (g_mkdir_with_parents (path_parent, 0777) != 0) {
1158 		g_set_error (error,
1159 			     AS_UTILS_ERROR,
1160 			     AS_UTILS_ERROR_FAILED,
1161 			     "Failed to create %s", path_parent);
1162 		return FALSE;
1163 	}
1164 
1165 	/* calculate the new destination */
1166 	file_src = g_file_new_for_path (filename);
1167 	basename = g_path_get_basename (filename);
1168 	if (origin != NULL) {
1169 		g_autofree gchar *basename_new = NULL;
1170 		tmp = g_strstr_len (basename, -1, ".");
1171 		if (tmp == NULL) {
1172 			g_set_error (error,
1173 				     AS_UTILS_ERROR,
1174 				     AS_UTILS_ERROR_FAILED,
1175 				     "Name of XML file invalid %s",
1176 				     basename);
1177 			return FALSE;
1178 		}
1179 		basename_new = g_strdup_printf ("%s%s", origin, tmp);
1180 		/* replace the fedora.xml.gz into %{origin}.xml.gz */
1181 		path_dest = g_build_filename (path_parent, basename_new, NULL);
1182 	} else {
1183 		path_dest = g_build_filename (path_parent, basename, NULL);
1184 	}
1185 
1186 	/* actually copy file */
1187 	file_dest = g_file_new_for_path (path_dest);
1188 	if (!g_file_copy (file_src, file_dest,
1189 			  G_FILE_COPY_OVERWRITE,
1190 			  NULL, NULL, NULL, error))
1191 		return FALSE;
1192 
1193 	/* fix the origin */
1194 	if (origin != NULL) {
1195 		g_autoptr(AsStore) store = NULL;
1196 		store = as_store_new ();
1197 		if (!as_store_from_file (store, file_dest, NULL, NULL, error))
1198 			return FALSE;
1199 		as_store_set_origin (store, origin);
1200 		if (!as_store_to_file (store, file_dest,
1201 				       AS_NODE_TO_XML_FLAG_ADD_HEADER |
1202 				       AS_NODE_TO_XML_FLAG_FORMAT_MULTILINE,
1203 				       NULL, error))
1204 			return FALSE;
1205 	}
1206 	return TRUE;
1207 }
1208 
1209 /**
1210  * as_utils_install_filename:
1211  * @location: the #AsUtilsLocation, e.g. %AS_UTILS_LOCATION_CACHE
1212  * @filename: the full path of the file to install
1213  * @origin: the origin to use for the installation, or %NULL
1214  * @destdir: the destdir to use, or %NULL
1215  * @error: A #GError or %NULL
1216  *
1217  * Installs an AppData, MetaInfo, AppStream XML or AppStream Icon metadata file.
1218  *
1219  * Returns: %TRUE for success, %FALSE if error is set
1220  *
1221  * Since: 0.3.4
1222  **/
1223 gboolean
as_utils_install_filename(AsUtilsLocation location,const gchar * filename,const gchar * origin,const gchar * destdir,GError ** error)1224 as_utils_install_filename (AsUtilsLocation location,
1225 			  const gchar *filename,
1226 			  const gchar *origin,
1227 			  const gchar *destdir,
1228 			  GError **error)
1229 {
1230 	gboolean ret = FALSE;
1231 	gchar *tmp;
1232 	g_autofree gchar *basename = NULL;
1233 	g_autofree gchar *path = NULL;
1234 
1235 	/* default value */
1236 	if (destdir == NULL)
1237 		destdir = "";
1238 
1239 	switch (as_format_guess_kind (filename)) {
1240 	case AS_FORMAT_KIND_APPSTREAM:
1241 		if (g_strstr_len (filename, -1, ".yml.gz") != NULL) {
1242 			path = g_build_filename (as_utils_location_get_prefix (location),
1243 						 "app-info", "yaml", NULL);
1244 			ret = as_utils_install_xml (filename, origin, path, destdir, error);
1245 		} else {
1246 			path = g_build_filename (as_utils_location_get_prefix (location),
1247 						 "app-info", "xmls", NULL);
1248 			ret = as_utils_install_xml (filename, origin, path, destdir, error);
1249 		}
1250 		break;
1251 	case AS_FORMAT_KIND_APPDATA:
1252 	case AS_FORMAT_KIND_METAINFO:
1253 		if (location == AS_UTILS_LOCATION_CACHE) {
1254 			g_set_error_literal (error,
1255 					     AS_UTILS_ERROR,
1256 					     AS_UTILS_ERROR_INVALID_TYPE,
1257 					     "cached location unsupported for "
1258 					     "MetaInfo and AppData files");
1259 			return FALSE;
1260 		}
1261 		path = g_build_filename (as_utils_location_get_prefix (location),
1262 					 "appdata", NULL);
1263 		ret = as_utils_install_xml (filename, NULL, path, destdir, error);
1264 		break;
1265 	default:
1266 		/* icons */
1267 		if (origin != NULL) {
1268 			ret = as_utils_install_icon (location, filename, origin, destdir, error);
1269 			break;
1270 		}
1271 		basename = g_path_get_basename (filename);
1272 		tmp = g_strstr_len (basename, -1, "-icons.tar.gz");
1273 		if (tmp != NULL) {
1274 			*tmp = '\0';
1275 			ret = as_utils_install_icon (location, filename, basename, destdir, error);
1276 			break;
1277 		}
1278 
1279 		/* unrecognised */
1280 		g_set_error_literal (error,
1281 				     AS_UTILS_ERROR,
1282 				     AS_UTILS_ERROR_INVALID_TYPE,
1283 				     "No idea how to process files of this type");
1284 		break;
1285 	}
1286 	return ret;
1287 }
1288 
1289 /**
1290  * as_utils_search_token_valid:
1291  * @token: the search token
1292  *
1293  * Checks the search token if it is valid. Valid tokens are at least 3 chars in
1294  * length, not common words like "and", and do not contain markup.
1295  *
1296  * Returns: %TRUE is the search token was valid
1297  *
1298  * Since: 0.3.4
1299  **/
1300 gboolean
as_utils_search_token_valid(const gchar * token)1301 as_utils_search_token_valid (const gchar *token)
1302 {
1303 	guint i;
1304 	for (i = 0; token[i] != '\0'; i++) {
1305 		if (token[i] == '<' ||
1306 		    token[i] == '>' ||
1307 		    token[i] == '(' ||
1308 		    token[i] == ')')
1309 			return FALSE;
1310 	}
1311 	if (i < 3)
1312 		return FALSE;
1313 	return TRUE;
1314 }
1315 
1316 /**
1317  * as_utils_search_tokenize:
1318  * @search: the search string
1319  *
1320  * Splits up a string into tokens and returns tokens that are suitable for
1321  * searching. This includes taking out common words and casefolding the
1322  * returned search tokens.
1323  *
1324  * Returns: (transfer full): Valid tokens to search for, or %NULL for error
1325  *
1326  * Since: 0.3.4
1327  **/
1328 gchar **
as_utils_search_tokenize(const gchar * search)1329 as_utils_search_tokenize (const gchar *search)
1330 {
1331 	gchar **values = NULL;
1332 	guint i;
1333 	guint idx = 0;
1334 	g_auto(GStrv) tmp = NULL;
1335 
1336 	/* only add keywords that are long enough */
1337 	tmp = g_strsplit (search, " ", -1);
1338 	values = g_new0 (gchar *, g_strv_length (tmp) + 1);
1339 	for (i = 0; tmp[i] != NULL; i++) {
1340 		if (!as_utils_search_token_valid (tmp[i]))
1341 			continue;
1342 		values[idx++] = g_utf8_casefold (tmp[i], -1);
1343 	}
1344 	if (idx == 0) {
1345 		g_free (values);
1346 		return NULL;
1347 	}
1348 	return values;
1349 }
1350 
1351 static gint
as_utils_vercmp_char(gchar chr1,gchar chr2)1352 as_utils_vercmp_char (gchar chr1, gchar chr2)
1353 {
1354 	if (chr1 == chr2)
1355 		return 0;
1356 	if (chr1 == '~')
1357 		return -1;
1358 	if (chr2 == '~')
1359 		return 1;
1360 	return chr1 < chr2 ? -1 : 1;
1361 }
1362 
1363 static gint
as_utils_vercmp_chunk(const gchar * str1,const gchar * str2)1364 as_utils_vercmp_chunk (const gchar *str1, const gchar *str2)
1365 {
1366 	guint i;
1367 
1368 	/* trivial */
1369 	if (g_strcmp0 (str1, str2) == 0)
1370 		return 0;
1371 	if (str1 == NULL)
1372 		return 1;
1373 	if (str2 == NULL)
1374 		return -1;
1375 
1376 	/* check each char of the chunk */
1377 	for (i = 0; str1[i] != '\0' && str2[i] != '\0'; i++) {
1378 		gint rc = as_utils_vercmp_char (str1[i], str2[i]);
1379 		if (rc != 0)
1380 			return rc;
1381 	}
1382 	return as_utils_vercmp_char (str1[i], str2[i]);
1383 }
1384 
1385 static gint
as_utils_vercmp_internal(const gchar * version_a,const gchar * version_b)1386 as_utils_vercmp_internal (const gchar *version_a,
1387                           const gchar *version_b)
1388 {
1389 	guint longest_split;
1390 	g_auto(GStrv) split_a = NULL;
1391 	g_auto(GStrv) split_b = NULL;
1392 
1393 	split_a = g_strsplit (version_a, ".", -1);
1394 	split_b = g_strsplit (version_b, ".", -1);
1395 
1396 	longest_split = MAX (g_strv_length (split_a), g_strv_length (split_b));
1397 	for (guint i = 0; i < longest_split; i++) {
1398 		gchar *endptr_a = NULL;
1399 		gchar *endptr_b = NULL;
1400 		gint64 ver_a;
1401 		gint64 ver_b;
1402 
1403 		/* we lost or gained a dot */
1404 		if (split_a[i] == NULL)
1405 			return -1;
1406 		if (split_b[i] == NULL)
1407 			return 1;
1408 
1409 		/* compare integers */
1410 		ver_a = g_ascii_strtoll (split_a[i], &endptr_a, 10);
1411 		ver_b = g_ascii_strtoll (split_b[i], &endptr_b, 10);
1412 		if (ver_a < ver_b)
1413 			return -1;
1414 		if (ver_a > ver_b)
1415 			return 1;
1416 
1417 		/* compare strings */
1418 		if ((endptr_a != NULL && endptr_a[0] != '\0') ||
1419 		    (endptr_b != NULL && endptr_b[0] != '\0')) {
1420 			gint rc = as_utils_vercmp_chunk (endptr_a, endptr_b);
1421 			if (rc < 0)
1422 				return -1;
1423 			if (rc > 0)
1424 				return 1;
1425 		}
1426 	}
1427 
1428 	/* we really shouldn't get here */
1429 	return 0;
1430 }
1431 
1432 /**
1433  * as_utils_vercmp_full:
1434  * @version_a: the release version, e.g. 1.2.3
1435  * @version_b: the release version, e.g. 1.2.3.1
1436  * @flags: some #AsVersionCompareFlag
1437  *
1438  * Compares version numbers for sorting.
1439  *
1440  * Returns: -1 if a < b, +1 if a > b, 0 if they are equal, and %G_MAXINT on error
1441  *
1442  * Since: 0.7.15
1443  */
1444 gint
as_utils_vercmp_full(const gchar * version_a,const gchar * version_b,AsVersionCompareFlag flags)1445 as_utils_vercmp_full (const gchar *version_a,
1446 		      const gchar *version_b,
1447 		      AsVersionCompareFlag flags)
1448 {
1449 	/* sanity check */
1450 	if (version_a == NULL || version_b == NULL)
1451 		return G_MAXINT;
1452 
1453 	/* optimisation */
1454 	if (g_strcmp0 (version_a, version_b) == 0)
1455 		return 0;
1456 
1457 	if (flags & AS_VERSION_COMPARE_FLAG_USE_HEURISTICS) {
1458 		/* split into sections, and try to parse */
1459 		g_autofree gchar *str_a = as_utils_version_parse (version_a);
1460 		g_autofree gchar *str_b = as_utils_version_parse (version_b);
1461 		return as_utils_vercmp_internal (str_a, str_b);
1462 	} else {
1463 #ifdef HAVE_RPM
1464 		return rpmvercmp (version_a, version_b);
1465 #else
1466 		return as_utils_vercmp_internal (version_a, version_b);
1467 #endif
1468 	}
1469 }
1470 
1471 /**
1472  * as_utils_vercmp:
1473  * @version_a: the release version, e.g. 1.2.3
1474  * @version_b: the release version, e.g. 1.2.3.1
1475  *
1476  * Compares version numbers for sorting.
1477  *
1478  * Returns: -1 if a < b, +1 if a > b, 0 if they are equal, and %G_MAXINT on error
1479  *
1480  * Since: 0.3.5
1481  */
1482 gint
as_utils_vercmp(const gchar * version_a,const gchar * version_b)1483 as_utils_vercmp (const gchar *version_a, const gchar *version_b)
1484 {
1485 	return as_utils_vercmp_full (version_a, version_b,
1486 				     AS_VERSION_COMPARE_FLAG_USE_HEURISTICS);
1487 }
1488 
1489 /**
1490  * as_ptr_array_find_string:
1491  * @array: gchar* array
1492  * @value: string to find
1493  *
1494  * Finds a string in a pointer array.
1495  *
1496  * Returns: the const string, or %NULL if not found
1497  **/
1498 const gchar *
as_ptr_array_find_string(GPtrArray * array,const gchar * value)1499 as_ptr_array_find_string (GPtrArray *array, const gchar *value)
1500 {
1501 	const gchar *tmp;
1502 	guint i;
1503 	for (i = 0; i < array->len; i++) {
1504 		tmp = g_ptr_array_index (array, i);
1505 		if (g_strcmp0 (tmp, value) == 0)
1506 			return tmp;
1507 	}
1508 	return NULL;
1509 }
1510 
1511 /**
1512  * as_utils_guid_from_data:
1513  * @namespace_id: A namespace ID, e.g. "6ba7b810-9dad-11d1-80b4-00c04fd430c8"
1514  * @data: data to hash
1515  * @data_len: length of @data
1516  * @error: A #GError or %NULL
1517  *
1518  * Returns a GUID for some data. This uses a hash and so even small
1519  * differences in the @data will produce radically different return values.
1520  *
1521  * The implementation is taken from RFC4122, Section 4.1.3; specifically
1522  * using a type-5 SHA-1 hash.
1523  *
1524  * Returns: A new GUID, or %NULL if the namespace_id was invalid
1525  *
1526  * Since: 0.6.13
1527  **/
1528 gchar *
as_utils_guid_from_data(const gchar * namespace_id,const guint8 * data,gsize data_len,GError ** error)1529 as_utils_guid_from_data (const gchar *namespace_id,
1530 			 const guint8 *data,
1531 			 gsize data_len,
1532 			 GError **error)
1533 {
1534 #ifdef _WIN32
1535 	g_set_error_literal (error,
1536 			     AS_UTILS_ERROR,
1537 			     AS_UTILS_ERROR_FAILED,
1538 			     "not supported");
1539 	return FALSE;
1540 #else
1541 	gchar guid_new[37]; /* 36 plus NUL */
1542 	gsize digestlen = 20;
1543 	guint8 hash[20];
1544 	gint rc;
1545 	uuid_t uu_namespace;
1546 	uuid_t uu_new;
1547 	g_autoptr(GChecksum) csum = NULL;
1548 
1549 	g_return_val_if_fail (namespace_id != NULL, FALSE);
1550 	g_return_val_if_fail (data != NULL, FALSE);
1551 	g_return_val_if_fail (data_len != 0, FALSE);
1552 
1553 	/* convert the namespace to binary */
1554 	rc = uuid_parse (namespace_id, uu_namespace);
1555 	if (rc != 0) {
1556 		g_set_error (error,
1557 			     AS_UTILS_ERROR,
1558 			     AS_UTILS_ERROR_FAILED,
1559 			     "namespace '%s' is invalid",
1560 			     namespace_id);
1561 		return FALSE;
1562 	}
1563 
1564 	/* hash the namespace and then the string */
1565 	csum = g_checksum_new (G_CHECKSUM_SHA1);
1566 	g_checksum_update (csum, (guchar *) uu_namespace, 16);
1567 	g_checksum_update (csum, (guchar *) data, (gssize) data_len);
1568 	g_checksum_get_digest (csum, hash, &digestlen);
1569 
1570 	/* copy most parts of the hash 1:1 */
1571 	memcpy (uu_new, hash, 16);
1572 
1573 	/* set specific bits according to Section 4.1.3 */
1574 	uu_new[6] = (guint8) ((uu_new[6] & 0x0f) | (5 << 4));
1575 	uu_new[8] = (guint8) ((uu_new[8] & 0x3f) | 0x80);
1576 
1577 	/* return as a string */
1578 	uuid_unparse (uu_new, guid_new);
1579 	return g_strdup (guid_new);
1580 #endif
1581 }
1582 
1583 /**
1584  * as_utils_guid_is_valid:
1585  * @guid: string to check
1586  *
1587  * Checks the source string is a valid string GUID descriptor.
1588  *
1589  * Returns: %TRUE if @guid was a valid GUID, %FALSE otherwise
1590  *
1591  * Since: 0.5.0
1592  **/
1593 gboolean
as_utils_guid_is_valid(const gchar * guid)1594 as_utils_guid_is_valid (const gchar *guid)
1595 {
1596 #ifdef _WIN32
1597 	/* XXX Ideally we should set a GError but this was already a public
1598 	 * API, and it doesn't have such parameter.
1599 	 */
1600 	g_printerr ("%s: not supported\n", G_STRFUNC);
1601 	return FALSE;
1602 #else
1603 	gint rc;
1604 	uuid_t uu;
1605 	if (guid == NULL)
1606 		return FALSE;
1607 	rc = uuid_parse (guid, uu);
1608 	return rc == 0;
1609 #endif
1610 }
1611 
1612 /**
1613  * as_utils_guid_from_string:
1614  * @str: A source string to use as a key
1615  *
1616  * Returns a GUID for a given string. This uses a hash and so even small
1617  * differences in the @str will produce radically different return values.
1618  *
1619  * The implementation is taken from RFC4122, Section 4.1.3; specifically
1620  * using a type-5 SHA-1 hash with a DNS namespace.
1621  * The same result can be obtained with this simple python program:
1622  *
1623  *    #!/usr/bin/python
1624  *    import uuid
1625  *    print uuid.uuid5(uuid.NAMESPACE_DNS, 'python.org')
1626  *
1627  * Returns: A new GUID, or %NULL if the string was invalid
1628  *
1629  * Since: 0.5.0
1630  **/
1631 gchar *
as_utils_guid_from_string(const gchar * str)1632 as_utils_guid_from_string (const gchar *str)
1633 {
1634 	if (str == NULL)
1635 		return NULL;
1636 	return as_utils_guid_from_data ("6ba7b810-9dad-11d1-80b4-00c04fd430c8",
1637 					(const guint8 *) str, strlen (str), NULL);
1638 }
1639 
1640 #define AS_UTILS_DECODE_BCD(val)	((((val) >> 4) & 0x0f) * 10 + ((val) & 0x0f))
1641 
1642 /**
1643  * as_utils_version_from_uint32:
1644  * @val: A uint32le version number
1645  * @flags: flags used for formatting, e.g. %AS_VERSION_PARSE_FLAG_USE_TRIPLET
1646  *
1647  * Returns a dotted decimal version string from a 32 bit number.
1648  *
1649  * Returns: A version number, e.g. "1.0.3"
1650  *
1651  * Since: 0.5.2
1652  **/
1653 gchar *
as_utils_version_from_uint32(guint32 val,AsVersionParseFlag flags)1654 as_utils_version_from_uint32 (guint32 val, AsVersionParseFlag flags)
1655 {
1656 	if (flags & AS_VERSION_PARSE_FLAG_USE_TRIPLET) {
1657 		return g_strdup_printf ("%u.%u.%u",
1658 					(val >> 24) & 0xff,
1659 					(val >> 16) & 0xff,
1660 					val & 0xffff);
1661 	}
1662 	return g_strdup_printf ("%u.%u.%u.%u",
1663 				(val >> 24) & 0xff,
1664 				(val >> 16) & 0xff,
1665 				(val >> 8) & 0xff,
1666 				val & 0xff);
1667 }
1668 
1669 /**
1670  * as_utils_version_from_uint16:
1671  * @val: A uint16le version number
1672  * @flags: flags used for formatting, e.g. %AS_VERSION_PARSE_FLAG_USE_TRIPLET
1673  *
1674  * Returns a dotted decimal version string from a 16 bit number.
1675  *
1676  * Returns: A version number, e.g. "1.3"
1677  *
1678  * Since: 0.5.2
1679  **/
1680 gchar *
as_utils_version_from_uint16(guint16 val,AsVersionParseFlag flags)1681 as_utils_version_from_uint16 (guint16 val, AsVersionParseFlag flags)
1682 {
1683 	if (flags & AS_VERSION_PARSE_FLAG_USE_BCD) {
1684 		return g_strdup_printf ("%u.%u",
1685 					AS_UTILS_DECODE_BCD(val >> 8),
1686 					AS_UTILS_DECODE_BCD(val));
1687 	}
1688 	return g_strdup_printf ("%u.%u",
1689 				(guint) (val >> 8) & 0xff,
1690 				(guint) val & 0xff);
1691 }
1692 
1693 /**
1694  * as_utils_version_parse:
1695  * @version: A version number
1696  *
1697  * Returns a dotted decimal version string from a version string. The supported
1698  * formats are:
1699  *
1700  * - Dotted decimal, e.g. "1.2.3"
1701  * - Base 16, a hex number *with* a 0x prefix, e.g. "0x10203"
1702  * - Base 10, a string containing just [0-9], e.g. "66051"
1703  * - Date in YYYYMMDD format, e.g. 20150915
1704  *
1705  * Anything with a '.' or that doesn't match [0-9] or 0x[a-f,0-9] is considered
1706  * a string and returned without modification.
1707  *
1708  * Returns: A version number, e.g. "1.0.3"
1709  *
1710  * Since: 0.5.2
1711  */
1712 gchar *
as_utils_version_parse(const gchar * version)1713 as_utils_version_parse (const gchar *version)
1714 {
1715 	const gchar *version_noprefix = version;
1716 	gchar *endptr = NULL;
1717 	guint64 tmp;
1718 	guint base;
1719 	guint i;
1720 
1721 	/* already dotted decimal */
1722 	if (g_strstr_len (version, -1, ".") != NULL)
1723 		return g_strdup (version);
1724 
1725 	/* is a date */
1726 	if (g_str_has_prefix (version, "20") &&
1727 	    strlen (version) == 8)
1728 		return g_strdup (version);
1729 
1730 	/* convert 0x prefixed strings to dotted decimal */
1731 	if (g_str_has_prefix (version, "0x")) {
1732 		version_noprefix += 2;
1733 		base = 16;
1734 	} else {
1735 		/* for non-numeric content, just return the string */
1736 		for (i = 0; version[i] != '\0'; i++) {
1737 			if (!g_ascii_isdigit (version[i]))
1738 				return g_strdup (version);
1739 		}
1740 		base = 10;
1741 	}
1742 
1743 	/* convert */
1744 	tmp = g_ascii_strtoull (version_noprefix, &endptr, base);
1745 	if (endptr != NULL && endptr[0] != '\0')
1746 		return g_strdup (version);
1747 	if (tmp == 0)
1748 		return g_strdup (version);
1749 	return as_utils_version_from_uint32 ((guint32) tmp, AS_VERSION_PARSE_FLAG_USE_TRIPLET);
1750 }
1751 
1752 /**
1753  * as_utils_string_replace:
1754  * @string: The #GString to operate on
1755  * @search: The text to search for
1756  * @replace: The text to use for substitutions
1757  *
1758  * Performs multiple search and replace operations on the given string.
1759  *
1760  * Returns: the number of replacements done, or 0 if @search is not found.
1761  *
1762  * Since: 0.5.11
1763  **/
1764 guint
as_utils_string_replace(GString * string,const gchar * search,const gchar * replace)1765 as_utils_string_replace (GString *string, const gchar *search, const gchar *replace)
1766 {
1767 	gchar *tmp;
1768 	guint count = 0;
1769 	gsize search_idx = 0;
1770 	gsize replace_len;
1771 	gsize search_len;
1772 
1773 	g_return_val_if_fail (string != NULL, 0);
1774 	g_return_val_if_fail (search != NULL, 0);
1775 	g_return_val_if_fail (replace != NULL, 0);
1776 
1777 	/* nothing to do */
1778 	if (string->len == 0)
1779 		return 0;
1780 
1781 	search_len = strlen (search);
1782 	replace_len = strlen (replace);
1783 
1784 	do {
1785 		tmp = g_strstr_len (string->str + search_idx, -1, search);
1786 		if (tmp == NULL)
1787 			break;
1788 
1789 		/* advance the counter in case @replace contains @search */
1790 		search_idx = (gsize) (tmp - string->str);
1791 
1792 		/* reallocate the string if required */
1793 		if (search_len > replace_len) {
1794 			g_string_erase (string,
1795 					(gssize) search_idx,
1796 					(gssize) (search_len - replace_len));
1797 			memcpy (tmp, replace, replace_len);
1798 		} else if (search_len < replace_len) {
1799 			g_string_insert_len (string,
1800 					     (gssize) search_idx,
1801 					     replace,
1802 					     (gssize) (replace_len - search_len));
1803 			/* we have to treat this specially as it could have
1804 			 * been reallocated when the insertion happened */
1805 			memcpy (string->str + search_idx, replace, replace_len);
1806 		} else {
1807 			/* just memcmp in the new string */
1808 			memcpy (tmp, replace, replace_len);
1809 		}
1810 		search_idx += replace_len;
1811 		count++;
1812 	} while (TRUE);
1813 
1814 	return count;
1815 }
1816 
1817 /**
1818  * as_utils_iso8601_to_datetime: (skip)
1819  * @iso_date: The ISO8601 date
1820  *
1821  * Converts an ISO8601 to a #GDateTime.
1822  *
1823  * Returns: a #GDateTime, or %NULL for error.
1824  *
1825  * Since: 0.6.1
1826  **/
1827 GDateTime *
as_utils_iso8601_to_datetime(const gchar * iso_date)1828 as_utils_iso8601_to_datetime (const gchar *iso_date)
1829 {
1830 	GTimeVal tv;
1831 	guint dmy[] = {0, 0, 0};
1832 
1833 	/* nothing set */
1834 	if (iso_date == NULL || iso_date[0] == '\0')
1835 		return NULL;
1836 
1837 	/* try to parse complete ISO8601 date */
1838 	if (g_strstr_len (iso_date, -1, " ") != NULL) {
1839 		if (g_time_val_from_iso8601 (iso_date, &tv) && tv.tv_sec != 0)
1840 			return g_date_time_new_from_timeval_utc (&tv);
1841 	}
1842 
1843 	/* g_time_val_from_iso8601() blows goats and won't
1844 	 * accept a valid ISO8601 formatted date without a
1845 	 * time value - try and parse this case */
1846 	if (sscanf (iso_date, "%u-%u-%u", &dmy[0], &dmy[1], &dmy[2]) != 3)
1847 		return NULL;
1848 
1849 	/* create valid object */
1850 	return g_date_time_new_utc ((gint) dmy[0], (gint) dmy[1], (gint) dmy[2], 0, 0, 0);
1851 }
1852 
1853 static const gchar *
_as_utils_fix_unique_id_part(const gchar * tmp)1854 _as_utils_fix_unique_id_part (const gchar *tmp)
1855 {
1856 	if (tmp == NULL || tmp[0] == '\0')
1857 		return AS_APP_UNIQUE_WILDCARD;
1858 	return tmp;
1859 }
1860 
1861 /**
1862  * as_utils_unique_id_build:
1863  * @scope: a #AsAppScope e.g. %AS_APP_SCOPE_SYSTEM
1864  * @bundle_kind: System, e.g. 'package' or 'flatpak'
1865  * @origin: Origin, e.g. 'fedora' or 'gnome-apps-nightly'
1866  * @kind: #AsAppKind, e.g. %AS_APP_KIND_DESKTOP
1867  * @id: AppStream ID, e.g. 'gimp.desktop'
1868  * @branch: Branch, e.g. '3-20' or 'master'
1869  *
1870  * Builds a valid unique ID using available data.
1871  *
1872  * Returns: a unique name, or %NULL for error;
1873  *
1874  * Since: 0.6.1
1875  */
1876 gchar *
as_utils_unique_id_build(AsAppScope scope,AsBundleKind bundle_kind,const gchar * origin,AsAppKind kind,const gchar * id,const gchar * branch)1877 as_utils_unique_id_build (AsAppScope scope,
1878 			  AsBundleKind bundle_kind,
1879 			  const gchar *origin,
1880 			  AsAppKind kind,
1881 			  const gchar *id,
1882 			  const gchar *branch)
1883 {
1884 	const gchar *bundle_str = NULL;
1885 	const gchar *kind_str = NULL;
1886 	const gchar *scope_str = NULL;
1887 
1888 	g_return_val_if_fail (id != NULL, NULL);
1889 
1890 	if (kind != AS_APP_KIND_UNKNOWN)
1891 		kind_str = as_app_kind_to_string (kind);
1892 	if (scope != AS_APP_SCOPE_UNKNOWN)
1893 		scope_str = as_app_scope_to_string (scope);
1894 	if (bundle_kind != AS_BUNDLE_KIND_UNKNOWN)
1895 		bundle_str = as_bundle_kind_to_string (bundle_kind);
1896 	return g_strdup_printf ("%s/%s/%s/%s/%s/%s",
1897 				_as_utils_fix_unique_id_part (scope_str),
1898 				_as_utils_fix_unique_id_part (bundle_str),
1899 				_as_utils_fix_unique_id_part (origin),
1900 				_as_utils_fix_unique_id_part (kind_str),
1901 				_as_utils_fix_unique_id_part (id),
1902 				_as_utils_fix_unique_id_part (branch));
1903 }
1904 
1905 static inline guint
as_utils_unique_id_find_part(const gchar * str)1906 as_utils_unique_id_find_part (const gchar *str)
1907 {
1908 	guint i;
1909 	for (i = 0; str[i] != '/' && str[i] != '\0'; i++);
1910 	return i;
1911 }
1912 
1913 /**
1914  * as_utils_unique_id_valid:
1915  * @unique_id: a unique ID
1916  *
1917  * Checks if a unique ID is valid i.e. has the correct number of
1918  * sections.
1919  *
1920  * Returns: %TRUE if the ID is valid
1921  *
1922  * Since: 0.6.1
1923  */
1924 gboolean
as_utils_unique_id_valid(const gchar * unique_id)1925 as_utils_unique_id_valid (const gchar *unique_id)
1926 {
1927 	guint i;
1928 	guint sections = 1;
1929 	if (unique_id == NULL)
1930 		return FALSE;
1931 	for (i = 0; unique_id[i] != '\0'; i++) {
1932 		if (unique_id[i] == '/')
1933 			sections++;
1934 	}
1935 	return sections == AS_UTILS_UNIQUE_ID_PARTS;
1936 }
1937 
1938 static inline gboolean
as_utils_unique_id_is_wildcard_part(const gchar * str,guint len)1939 as_utils_unique_id_is_wildcard_part (const gchar *str, guint len)
1940 {
1941 	return len == 1 && str[0] == '*';
1942 }
1943 
1944 /**
1945  * as_utils_unique_id_match:
1946  * @unique_id1: a unique ID
1947  * @unique_id2: another unique ID
1948  * @match_flags: a #AsUniqueIdMatchFlags bitfield, e.g. %AS_UNIQUE_ID_MATCH_FLAG_ID
1949  *
1950  * Checks two unique IDs for equality allowing globs to match, whilst also
1951  * allowing clients to whitelist sections that have to match.
1952  *
1953  * Returns: %TRUE if the ID's should be considered equal.
1954  *
1955  * Since: 0.7.8
1956  */
1957 gboolean
as_utils_unique_id_match(const gchar * unique_id1,const gchar * unique_id2,AsUniqueIdMatchFlags match_flags)1958 as_utils_unique_id_match (const gchar *unique_id1,
1959 			  const gchar *unique_id2,
1960 			  AsUniqueIdMatchFlags match_flags)
1961 {
1962 	guint last1 = 0;
1963 	guint last2 = 0;
1964 	guint len1;
1965 	guint len2;
1966 
1967 	/* trivial */
1968 	if (unique_id1 == unique_id2)
1969 		return TRUE;
1970 
1971 	/* invalid */
1972 	if (!as_utils_unique_id_valid (unique_id1) ||
1973 	    !as_utils_unique_id_valid (unique_id2))
1974 		return g_strcmp0 (unique_id1, unique_id2) == 0;
1975 
1976 	/* look at each part */
1977 	for (guint i = 0; i < AS_UTILS_UNIQUE_ID_PARTS; i++) {
1978 		const gchar *tmp1 = unique_id1 + last1;
1979 		const gchar *tmp2 = unique_id2 + last2;
1980 
1981 		/* find the slash or the end of the string */
1982 		len1 = as_utils_unique_id_find_part (tmp1);
1983 		len2 = as_utils_unique_id_find_part (tmp2);
1984 
1985 		/* either string was a wildcard */
1986 		if (match_flags & (1 << i) &&
1987 		    !as_utils_unique_id_is_wildcard_part (tmp1, len1) &&
1988 		    !as_utils_unique_id_is_wildcard_part (tmp2, len2)) {
1989 			/* are substrings the same */
1990 			if (len1 != len2)
1991 				return FALSE;
1992 			if (memcmp (tmp1, tmp2, len1) != 0)
1993 				return FALSE;
1994 		}
1995 
1996 		/* advance to next section */
1997 		last1 += len1 + 1;
1998 		last2 += len2 + 1;
1999 	}
2000 	return TRUE;
2001 }
2002 
2003 /**
2004  * as_utils_unique_id_equal:
2005  * @unique_id1: a unique ID
2006  * @unique_id2: another unique ID
2007  *
2008  * Checks two unique IDs for equality allowing globs to match.
2009  *
2010  * Returns: %TRUE if the ID's should be considered equal.
2011  *
2012  * Since: 0.6.1
2013  */
2014 gboolean
as_utils_unique_id_equal(const gchar * unique_id1,const gchar * unique_id2)2015 as_utils_unique_id_equal (const gchar *unique_id1, const gchar *unique_id2)
2016 {
2017 	return as_utils_unique_id_match (unique_id1,
2018 					 unique_id2,
2019 					 AS_UNIQUE_ID_MATCH_FLAG_SCOPE |
2020 					 AS_UNIQUE_ID_MATCH_FLAG_BUNDLE_KIND |
2021 					 AS_UNIQUE_ID_MATCH_FLAG_ORIGIN |
2022 					 AS_UNIQUE_ID_MATCH_FLAG_KIND |
2023 					 AS_UNIQUE_ID_MATCH_FLAG_ID |
2024 					 AS_UNIQUE_ID_MATCH_FLAG_BRANCH);
2025 }
2026 
2027 /**
2028  * as_utils_unique_id_hash:
2029  * @unique_id: a unique ID
2030  *
2031  * Converts a unique-id to a hash value.
2032  *
2033  * This function implements the widely used DJB hash on the ID subset of the
2034  * unique-id string.
2035  *
2036  * It can be passed to g_hash_table_new() as the hash_func parameter,
2037  * when using non-NULL strings or unique_ids as keys in a GHashTable.
2038  *
2039  * Returns: a hash value corresponding to the key
2040  *
2041  * Since: 0.6.2
2042  */
2043 guint
as_utils_unique_id_hash(const gchar * unique_id)2044 as_utils_unique_id_hash (const gchar *unique_id)
2045 {
2046 	gsize i;
2047 	guint hash = 5381;
2048 	guint section_cnt = 0;
2049 
2050 	/* not a unique ID */
2051 	if (!as_utils_unique_id_valid (unique_id))
2052 		return g_str_hash (unique_id);
2053 
2054 	/* only include the app-id */
2055 	for (i = 0; unique_id[i] != '\0'; i++) {
2056 		if (unique_id[i] == '/') {
2057 			if (++section_cnt > 4)
2058 				break;
2059 			continue;
2060 		}
2061 		if (section_cnt < 4)
2062 			continue;
2063 		hash = (guint) ((hash << 5) + hash) + (guint) (unique_id[i]);
2064 	}
2065 	return hash;
2066 }
2067 
2068 static gboolean
as_utils_appstream_id_is_valid_char(gchar ch)2069 as_utils_appstream_id_is_valid_char (gchar ch)
2070 {
2071 	if (g_ascii_isalnum (ch))
2072 		return TRUE;
2073 	if (ch == '.')
2074 		return TRUE;
2075 	if (ch == '-')
2076 		return TRUE;
2077 	return FALSE;
2078 }
2079 
2080 /**
2081  * as_utils_appstream_id_build:
2082  * @str: a string to build the AppStream ID from
2083  *
2084  * Fixes a string to be a valid AppStream ID.
2085  *
2086  * This function replaces any invalid chars with an underscore.
2087  *
2088  * Returns: a valid AppStream ID, or %NULL if @str is invalid
2089  *
2090  * Since: 0.6.4
2091  */
2092 gchar *
as_utils_appstream_id_build(const gchar * str)2093 as_utils_appstream_id_build (const gchar *str)
2094 {
2095 	gchar *tmp;
2096 	guint i;
2097 
2098 	/* invalid */
2099 	if (str == NULL)
2100 		return NULL;
2101 	if (str[0] == '\0')
2102 		return NULL;
2103 
2104 	tmp = g_strdup (str);
2105 	for (i = 0; tmp[i] != '\0'; i++) {
2106 		if (!as_utils_appstream_id_is_valid_char (tmp[i]))
2107 			tmp[i] = '_';
2108 	}
2109 	return tmp;
2110 }
2111 
2112 /**
2113  * as_utils_appstream_id_valid:
2114  * @str: a string
2115  *
2116  * Checks to see if a string is a valid AppStream ID. A valid AppStream ID only
2117  * contains alphanumeric chars, dots and dashes.
2118  *
2119  * Returns: %TRUE if the string is a valid AppStream ID
2120  *
2121  * Since: 0.6.4
2122  */
2123 gboolean
as_utils_appstream_id_valid(const gchar * str)2124 as_utils_appstream_id_valid (const gchar *str)
2125 {
2126 	guint i;
2127 	for (i = 0; str[i] != '\0'; i++) {
2128 		if (!as_utils_appstream_id_is_valid_char (str[i]))
2129 			return FALSE;
2130 	}
2131 	return TRUE;
2132 }
2133