Clarify special directories
[appimagekit/gsi.git] / shared.c
blob4e85d4a8cb0f07aa0fb957ec4ab619af52cb2404
1 /**************************************************************************
2 *
3 * Copyright (c) 2004-17 Simon Peter
4 *
5 * All Rights Reserved.
6 *
7 * Permission is hereby granted, free of charge, to any person obtaining a copy
8 * of this software and associated documentation files (the "Software"), to deal
9 * in the Software without restriction, including without limitation the rights
10 * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
11 * copies of the Software, and to permit persons to whom the Software is
12 * furnished to do so, subject to the following conditions:
14 * The above copyright notice and this permission notice shall be included in
15 * all copies or substantial portions of the Software.
17 * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
18 * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
19 * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
20 * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
21 * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
22 * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
23 * THE SOFTWARE.
25 **************************************************************************/
27 #ident "AppImage by Simon Peter, http://appimage.org/"
29 #include <stdio.h>
30 #include <string.h>
31 #include <stdlib.h>
32 #include <unistd.h>
33 #include <sys/types.h>
34 #include <dirent.h>
35 #include <stdio.h>
36 #include <errno.h>
38 #include <glib.h>
39 #include <glib/gprintf.h>
40 #include <glib/gstdio.h>
41 #include <gio/gio.h>
43 #include "squashfuse.h"
44 #include <squashfs_fs.h>
46 #include "elf.h"
47 #include "getsection.h"
49 #if HAVE_LIBARCHIVE3 == 1 // CentOS
50 # include <archive3.h>
51 # include <archive_entry3.h>
52 #else // other systems
53 # include <archive.h>
54 # include <archive_entry.h>
55 #endif
57 #include <regex.h>
59 #include <cairo.h> // To get the size of icons, move_icon_to_destination()
61 #define FNM_FILE_NAME 2
63 #define URI_MAX (FILE_MAX * 3 + 8)
65 char *vendorprefix = "appimagekit";
67 void set_executable(char *path, gboolean verbose)
69 if(!g_find_program_in_path ("firejail")){
70 int result = chmod(path, 0755); // TODO: Only do this if signature verification passed
71 if(result != 0){
72 fprintf(stderr, "Could not set %s executable: %s\n", path, strerror(errno));
73 } else {
74 if(verbose)
75 fprintf(stderr, "Set %s executable\n", path);
80 /* Search and replace on a string, this really should be in Glib
81 * https://mail.gnome.org/archives/gtk-list/2012-February/msg00005.html */
82 gchar* replace_str(const gchar *src, const gchar *find, const gchar *replace){
83 gchar* retval = g_strdup(src);
84 gchar* ptr = NULL;
85 ptr = g_strstr_len(retval,-1,find);
86 if (ptr != NULL){
87 gchar* after_find = replace_str(ptr+strlen(find),find,replace);
88 gchar* before_find = g_strndup(retval,ptr-retval);
89 gchar* temp = g_strconcat(before_find,replace,after_find,NULL);
90 g_free(retval);
91 retval = g_strdup(temp);
92 g_free(before_find);
93 g_free(temp);
95 return retval;
98 /* Return the md5 hash constructed according to
99 * https://specifications.freedesktop.org/thumbnail-spec/thumbnail-spec-latest.html#THUMBSAVE
100 * This can be used to identify files that are related to a given AppImage at a given location */
101 char * get_md5(char *path)
103 gchar *uri = g_filename_to_uri (path, NULL, NULL);
104 GChecksum *checksum;
105 checksum = g_checksum_new(G_CHECKSUM_MD5);
106 guint8 digest[16];
107 gsize digest_len = sizeof (digest);
108 g_checksum_update(checksum, (const guchar *) uri, strlen (uri));
109 g_checksum_get_digest(checksum, digest, &digest_len);
110 g_assert(digest_len == 16);
111 return g_strdup_printf("%s", g_checksum_get_string(checksum));
114 /* Return the path of the thumbnail regardless whether it already exists; may be useful because
115 * G*_FILE_ATTRIBUTE_THUMBNAIL_PATH only exists if the thumbnail is already there.
116 * Check libgnomeui/gnome-thumbnail.h for actually generating thumbnails in the correct
117 * sizes at the correct locations automatically; which would draw in a dependency on gdk-pixbuf.
119 char * get_thumbnail_path(char *path, char *thumbnail_size, gboolean verbose)
121 char *file;
122 file = g_strconcat (get_md5(path), ".png", NULL);
123 gchar *thumbnail_path = g_build_filename (g_get_user_cache_dir(), "thumbnails", thumbnail_size, file, NULL);
124 g_free (file);
125 return thumbnail_path;
128 /* Move an icon file to the path where a given icon can be installed in $HOME.
129 * This is needed because png and xpm icons cannot be installed in a generic
130 * location but are only picked up in directories that have the size of
131 * the icon as part of their directory name, as specified in the theme.index
132 * See https://github.com/AppImage/AppImageKit/issues/258
135 void move_icon_to_destination(gchar *icon_path, gboolean verbose)
137 // FIXME: This default location is most likely wrong, but at least the icons with unknown size can go somewhere
138 gchar *dest_dir = dest_dir = g_build_path("/", g_get_user_data_dir(), "/icons/hicolor/48x48/apps", NULL);;
140 if((g_str_has_suffix (icon_path, ".svg")) || (g_str_has_suffix (icon_path, ".svgz"))) {
141 dest_dir = g_build_path("/", g_get_user_data_dir(), "/icons/hicolor/scalable/apps/", NULL);
144 if((g_str_has_suffix (icon_path, ".png")) || (g_str_has_suffix (icon_path, ".xpm"))) {
146 cairo_surface_t *image;
148 int w = 0;
149 int h = 0;
151 if(g_str_has_suffix (icon_path, ".xpm")) {
152 // TODO: GdkPixbuf has a convenient way to load XPM data. Then you can call
153 // gdk_cairo_set_source_pixbuf() to transfer the data to a Cairo surface.
154 fprintf(stderr, "XPM size parsing not yet implemented\n");
158 if(g_str_has_suffix (icon_path, ".png")) {
159 image = cairo_image_surface_create_from_png(icon_path);
160 w = cairo_image_surface_get_width (image);
161 h = cairo_image_surface_get_height (image);
162 cairo_surface_destroy (image);
165 // FIXME: The following sizes are taken from the hicolor icon theme.
166 // Probably the right thing to do would be to figure out at runtime which icon sizes are allowable.
167 // Or could we put our own index.theme into .local/share/icons/ and have it observed?
168 if((w != h) || ((w != 16) && (w != 24) && (w != 32) && (w != 36) && (w != 48) && (w != 64) && (w != 72) && (w != 96) && (w != 128) && (w != 192) && (w != 256) && (w != 512))){
169 fprintf(stderr, "%s has nonstandard size w = %i, h = %i; please fix it\n", icon_path, w, h);
170 } else {
171 dest_dir = g_build_path("/", g_get_user_data_dir(), "/icons/hicolor/", g_strdup_printf("%ix%i", w, h), "/apps", NULL);
174 if(verbose)
175 fprintf(stderr, "dest_dir %s\n", dest_dir);
177 gchar* icon_dest_path = g_build_path("/", dest_dir, g_path_get_basename(icon_path), NULL);
178 if(verbose)
179 fprintf(stderr, "Move from %s to %s\n", icon_path, icon_dest_path);
180 if(g_mkdir_with_parents(g_path_get_dirname(dest_dir), 0755))
181 fprintf(stderr, "Could not create directory: %s\n", dest_dir);
182 GError *error = NULL;
183 GFile *icon_file = g_file_new_for_path(icon_path);
184 GFile *target_file = g_file_new_for_path(icon_dest_path);
185 if (!g_file_move (icon_file, target_file, G_FILE_COPY_OVERWRITE, NULL, NULL, NULL, &error)) {
186 fprintf(stderr, "Error moving file: %s\n", error->message);
187 g_error_free (error);
189 g_object_unref(icon_file);
190 g_object_unref(target_file);
194 /* Check if a file is an AppImage. Returns the image type if it is, or -1 if it isn't */
195 int check_appimage_type(char *path, gboolean verbose)
197 char buffer[3];
198 FILE *f = fopen(path, "rt");
199 if (f != NULL)
201 /* Check magic bytes at offset 8 */
202 fseek(f, 8, SEEK_SET);
203 fread(buffer, 1, 3, f);
204 fclose(f);
205 if((buffer[0] == 0x41) && (buffer[1] == 0x49) && (buffer[2] == 0x01)){
206 fprintf(stderr, "_________________________\n");
207 if(verbose){
208 fprintf(stderr, "AppImage type 1\n");
210 return 1;
211 } else if((buffer[0] == 0x41) && (buffer[1] == 0x49) && (buffer[2] == 0x02)){
212 fprintf(stderr, "_________________________\n");
213 if(verbose){
214 fprintf(stderr, "AppImage type 2\n");
216 return 2;
217 } else {
218 if((g_str_has_suffix(path,".AppImage")) || (g_str_has_suffix(path,".appimage"))) {
219 fprintf(stderr, "_________________________\n");
220 fprintf(stderr, "Blindly assuming AppImage type 1\n");
221 fprintf(stderr, "The author of this AppImage should embed the magic bytes, see https://github.com/AppImage/AppImageSpec\n");
222 return 1;
223 } else {
224 if(verbose){
225 fprintf(stderr, "_________________________\n");
226 fprintf(stderr, "Unrecognized file '%s'\n", path);
228 return -1;
232 return -1;
235 /* Get filename extension */
236 static gchar *get_file_extension(const gchar *filename)
238 gchar **tokens;
239 gchar *extension;
240 tokens = g_strsplit(filename, ".", 2);
241 if (tokens[0] == NULL)
242 extension = NULL;
243 else
244 extension = g_strdup(tokens[1]);
245 g_strfreev(tokens);
246 return extension;
249 /* Find files in the squashfs matching to the regex pattern.
250 * Returns a newly-allocated NULL-terminated array of strings.
251 * Use g_strfreev() to free it.
253 * The following is done within the sqfs_traverse run for performance reaons:
254 * 1.) For found files that are in usr/share/icons, install those icons into the system
255 * with a custom name that involves the md5 identifier to tie them to a particular
256 * AppImage.
257 * 2.) For found files that are in usr/share/mime/packages, install those icons into the system
258 * with a custom name that involves the md5 identifier to tie them to a particular
259 * AppImage.
261 gchar **squash_get_matching_files(sqfs *fs, char *pattern, gchar *desktop_icon_value_original, char *md5, gboolean verbose) {
262 GPtrArray *array = g_ptr_array_new();
263 sqfs_traverse trv;
264 sqfs_err err = sqfs_traverse_open(&trv, fs, sqfs_inode_root(fs));
265 if (err!= SQFS_OK)
266 fprintf(stderr, "sqfs_traverse_open error\n");
267 while (sqfs_traverse_next(&trv, &err)) {
268 if (!trv.dir_end) {
269 int r;
270 regex_t regex;
271 regmatch_t match[2];
272 regcomp(&regex, pattern, REG_ICASE | REG_EXTENDED);
273 r = regexec(&regex, trv.path, 2, match, 0);
274 regfree(&regex);
275 sqfs_inode inode;
276 if(sqfs_inode_get(fs, &inode, trv.entry.inode))
277 fprintf(stderr, "sqfs_inode_get error\n");
278 if(r == 0){
279 // We have a match
280 if(verbose)
281 fprintf(stderr, "squash_get_matching_files found: %s\n", trv.path);
282 g_ptr_array_add(array, g_strdup(trv.path));
283 gchar *dest = NULL;
284 gchar *dest_dirname;
285 gchar *dest_basename;
286 if(inode.base.inode_type == SQUASHFS_REG_TYPE) {
287 if(g_str_has_prefix(trv.path, "usr/share/icons/") || g_str_has_prefix(trv.path, "usr/share/pixmaps/") || (g_str_has_prefix(trv.path, "usr/share/mime/") && g_str_has_suffix(trv.path, ".xml"))){
288 dest_dirname = g_path_get_dirname(replace_str(trv.path, "usr/share", g_get_user_data_dir()));
289 dest_basename = g_strdup_printf("%s_%s_%s", vendorprefix, md5, g_path_get_basename(trv.path));
290 dest = g_build_path("/", dest_dirname, dest_basename, NULL);
292 /* According to https://specifications.freedesktop.org/icon-theme-spec/icon-theme-spec-latest.html#install_icons
293 * share/pixmaps is ONLY searched in /usr but not in $XDG_DATA_DIRS and hence $HOME and this seems to be true at least in XFCE */
294 if(g_str_has_prefix (trv.path, "usr/share/pixmaps/")){
295 dest_basename = g_strdup_printf("%s_%s_%s", vendorprefix, md5, g_path_get_basename(trv.path));
296 dest = g_build_path("/", "/tmp", dest_basename, NULL);
298 /* Some AppImages only have the icon in their root directory, so we have to get it from there */
299 if((g_str_has_prefix(trv.path, desktop_icon_value_original)) && (! strstr(trv.path, "/")) && ( (g_str_has_suffix(trv.path, ".png")) || (g_str_has_suffix(trv.path, ".xpm")) || (g_str_has_suffix(trv.path, ".svg")) || (g_str_has_suffix(trv.path, ".svgz")))){
300 dest_basename = g_strdup_printf("%s_%s_%s.%s", vendorprefix, md5, desktop_icon_value_original, get_file_extension(trv.path));
301 dest = g_build_path("/", "/tmp", dest_basename, NULL);
304 if(dest){
305 if(verbose)
306 fprintf(stderr, "install: %s\n", dest);
307 if(g_mkdir_with_parents(g_path_get_dirname(dest), 0755))
308 fprintf(stderr, "Could not create directory: %s\n", g_path_get_dirname(dest));
310 // Read the file in chunks
311 off_t bytes_already_read = 0;
312 sqfs_off_t bytes_at_a_time = 64*1024;
313 FILE * f;
314 f = fopen (dest, "w+");
315 if (f == NULL){
316 fprintf(stderr, "fopen error\n");
317 break;
319 while (bytes_already_read < inode.xtra.reg.file_size)
321 char buf[bytes_at_a_time];
322 if (sqfs_read_range(fs, &inode, (sqfs_off_t) bytes_already_read, &bytes_at_a_time, buf))
323 fprintf(stderr, "sqfs_read_range error\n");
324 fwrite(buf, 1, bytes_at_a_time, f);
325 bytes_already_read = bytes_already_read + bytes_at_a_time;
327 fclose(f);
328 chmod (dest, 0644);
330 if(verbose)
331 fprintf(stderr, "Installed: %s\n", dest);
333 // If we were unsure about the size of an icon, we temporarily installed
334 // it to /tmp and now move it into the proper place
335 if(g_str_has_prefix (dest, "/tmp/")) {
336 move_icon_to_destination(dest, verbose);
343 g_ptr_array_add(array, NULL);
344 if (err)
345 fprintf(stderr, "sqfs_traverse_next error\n");
346 sqfs_traverse_close(&trv);
347 return (gchar **) g_ptr_array_free(array, FALSE);
350 /* Loads a desktop file from squashfs into an empty GKeyFile structure.
351 * FIXME: Use sqfs_lookup_path() instead of g_key_file_load_from_squash()
352 * should help for performance. Please submit a pull request if you can
353 * get it to work.
355 gboolean g_key_file_load_from_squash(sqfs *fs, char *path, GKeyFile *key_file_structure, gboolean verbose) {
356 sqfs_traverse trv;
357 gboolean success = true;
358 sqfs_err err = sqfs_traverse_open(&trv, fs, sqfs_inode_root(fs));
359 if (err != SQFS_OK)
360 fprintf(stderr, "sqfs_traverse_open error\n");
361 while (sqfs_traverse_next(&trv, &err)) {
362 if (!trv.dir_end) {
363 if (strcmp(path, trv.path) == 0){
364 sqfs_inode inode;
365 if (sqfs_inode_get(fs, &inode, trv.entry.inode))
366 fprintf(stderr, "sqfs_inode_get error\n");
367 if (inode.base.inode_type == SQUASHFS_REG_TYPE){
368 off_t bytes_already_read = 0;
369 sqfs_off_t max_bytes_to_read = 256*1024;
370 char buf[max_bytes_to_read];
371 if (sqfs_read_range(fs, &inode, (sqfs_off_t) bytes_already_read, &max_bytes_to_read, buf))
372 fprintf(stderr, "sqfs_read_range error\n");
373 // fwrite(buf, 1, max_bytes_to_read, stdout);
374 success = g_key_file_load_from_data (key_file_structure, buf, max_bytes_to_read, G_KEY_FILE_KEEP_COMMENTS | G_KEY_FILE_KEEP_TRANSLATIONS, NULL);
375 } else {
376 fprintf(stderr, "TODO: Implement inode.base.inode_type %i\n", inode.base.inode_type);
378 break;
383 if (err)
384 fprintf(stderr, "sqfs_traverse_next error\n");
385 sqfs_traverse_close(&trv);
387 return success;
390 /* Write a modified desktop file to disk that points to the AppImage */
391 void write_edited_desktop_file(GKeyFile *key_file_structure, char* appimage_path, gchar* desktop_filename, int appimage_type, char *md5, gboolean verbose){
392 if(!g_key_file_has_key(key_file_structure, G_KEY_FILE_DESKTOP_GROUP, G_KEY_FILE_DESKTOP_KEY_EXEC, NULL)){
393 fprintf(stderr, "Desktop file has no Exec key\n");
394 return;
396 g_key_file_set_value(key_file_structure, G_KEY_FILE_DESKTOP_GROUP, G_KEY_FILE_DESKTOP_KEY_EXEC, appimage_path);
397 //gchar *tryexec_path = replace_str(appimage_path," ", "\\ "); // TryExec does not support blanks
398 g_key_file_set_value(key_file_structure, G_KEY_FILE_DESKTOP_GROUP, G_KEY_FILE_DESKTOP_KEY_TRY_EXEC, appimage_path);
400 /* If firejail is on the $PATH, then use it to run AppImages */
401 if(g_find_program_in_path ("firejail")){
402 char *firejail_exec;
403 firejail_exec = g_strdup_printf("firejail --env=DESKTOPINTEGRATION=appimaged --noprofile --appimage '%s'", appimage_path);
404 g_key_file_set_value(key_file_structure, G_KEY_FILE_DESKTOP_GROUP, G_KEY_FILE_DESKTOP_KEY_EXEC, firejail_exec);
406 gchar *firejail_profile_group = "Desktop Action FirejailProfile";
407 gchar *firejail_profile_exec = g_strdup_printf("firejail --env=DESKTOPINTEGRATION=appimaged --private --appimage '%s'", appimage_path);
408 gchar *firejail_tryexec = "firejail";
409 g_key_file_set_value(key_file_structure, firejail_profile_group, G_KEY_FILE_DESKTOP_KEY_NAME, "Run without sandbox profile");
410 g_key_file_set_value(key_file_structure, firejail_profile_group, G_KEY_FILE_DESKTOP_KEY_EXEC, firejail_profile_exec);
411 g_key_file_set_value(key_file_structure, firejail_profile_group, G_KEY_FILE_DESKTOP_KEY_TRY_EXEC, firejail_tryexec);
412 g_key_file_set_value(key_file_structure, G_KEY_FILE_DESKTOP_GROUP, "Actions", "FirejailProfile;");
416 /* Add AppImageUpdate desktop action
417 * https://specifications.freedesktop.org/desktop-entry-spec/latest/ar01s10.html
418 * This will only work if AppImageUpdate is on the user's $PATH.
419 * TODO: we could have it call this appimaged instance instead of AppImageUpdate and let it
420 * figure out how to update the AppImage */
421 unsigned long upd_offset = 0;
422 unsigned long upd_length = 0;
423 if(g_find_program_in_path ("AppImageUpdate")){
424 if(appimage_type == 2){
425 get_elf_section_offset_and_lenghth(appimage_path, ".upd_info", &upd_offset, &upd_length);
426 if(upd_length != 1024)
427 fprintf(stderr, "WARNING: .upd_info length is %lu rather than 1024, this might be a bug in the AppImage\n", upd_length);
429 if(appimage_type == 1){
430 /* If we have a type 1 AppImage, then we hardcode the offset and length */
431 upd_offset = 33651; // ISO 9660 Volume Descriptor field
432 upd_length = 512; // Might be wrong
434 fprintf(stderr, ".upd_info offset: %lu\n", upd_offset);
435 fprintf(stderr, ".upd_info length: %lu\n", upd_length);
436 char buffer[3];
437 FILE *binary = fopen(appimage_path, "rt");
438 if (binary != NULL)
440 /* Check whether the first three bytes at the offset are not NULL */
441 fseek(binary, upd_offset, SEEK_SET);
442 fread(buffer, 1, 3, binary);
443 fclose(binary);
444 if((buffer[0] != 0x00) && (buffer[1] != 0x00) && (buffer[2] != 0x00)){
445 gchar *appimageupdate_group = "Desktop Action AppImageUpdate";
446 gchar *appimageupdate_exec = g_strdup_printf("%s %s", "AppImageUpdate", appimage_path);
447 g_key_file_set_value(key_file_structure, appimageupdate_group, G_KEY_FILE_DESKTOP_KEY_NAME, "Update");
448 g_key_file_set_value(key_file_structure, appimageupdate_group, G_KEY_FILE_DESKTOP_KEY_EXEC, appimageupdate_exec);
449 g_key_file_set_value(key_file_structure, G_KEY_FILE_DESKTOP_GROUP, "Actions", "AppImageUpdate;");
454 gchar *icon_with_md5 = g_strdup_printf("%s_%s_%s", vendorprefix, md5, g_path_get_basename(g_key_file_get_value(key_file_structure, "Desktop Entry", "Icon", NULL)));
455 g_key_file_set_value(key_file_structure, "Desktop Entry", "Icon", icon_with_md5);
456 /* At compile time, inject VERSION_NUMBER like this:
457 * cc ... -DVERSION_NUMBER=\"$(git describe --tags --always --abbrev=7)\" -..
459 gchar *generated_by = g_strdup_printf("Generated by appimaged %s", VERSION_NUMBER);
460 g_key_file_set_value(key_file_structure, "Desktop Entry", "X-AppImage-Comment", generated_by);
461 g_key_file_set_value(key_file_structure, "Desktop Entry", "X-AppImage-Identifier", md5);
462 fprintf(stderr, "Installing desktop file\n");
463 if(verbose)
464 fprintf(stderr, "%s", g_key_file_to_data(key_file_structure, NULL, NULL));
466 /* https://specifications.freedesktop.org/menu-spec/menu-spec-latest.html#paths says:
468 * $XDG_DATA_DIRS/applications/
469 * When two desktop entries have the same name, the one appearing earlier in the path is used.
471 * --
473 * https://developer.gnome.org/integration-guide/stable/desktop-files.html.en says:
475 * Place this file in the /usr/share/applications directory so that it is accessible
476 * by everyone, or in ~/.local/share/applications if you only wish to make it accessible
477 * to a single user. Which is used should depend on whether your application is
478 * installed systemwide or into a user's home directory. GNOME monitors these directories
479 * for changes, so simply copying the file to the right location is enough to register it
480 * with the desktop.
482 * Note that the ~/.local/share/applications location is not monitored by versions of GNOME
483 * prior to version 2.10 or on Fedora Core Linux, prior to version 2.8.
484 * These versions of GNOME follow the now-deprecated vfolder standard,
485 * and so desktop files must be installed to ~/.gnome2/vfolders/applications.
486 * This location is not supported by GNOME 2.8 on Fedora Core nor on upstream GNOME 2.10
487 * so for maximum compatibility with deployed desktops, put the file in both locations.
489 * Note that the KDE Desktop requires one to run kbuildsycoca to force a refresh of the menus.
491 * --
493 * https://specifications.freedesktop.org/menu-spec/menu-spec-latest.html says:
495 * To prevent that a desktop entry from one party inadvertently cancels out
496 * the desktop entry from another party because both happen to get the same
497 * desktop-file id it is recommended that providers of desktop-files ensure
498 * that all desktop-file ids start with a vendor prefix.
499 * A vendor prefix consists of [a-zA-Z] and is terminated with a dash ("-").
500 * For example, to ensure that GNOME applications start with a vendor prefix of "gnome-",
501 * it could either add "gnome-" to all the desktop files it installs
502 * in datadir/applications/ or it could install desktop files in a
503 * datadir/applications/gnome subdirectory.
505 * --
507 * https://specifications.freedesktop.org/desktop-entry-spec/latest/ape.html says:
508 * The desktop file ID is the identifier of an installed desktop entry file.
510 * To determine the ID of a desktop file, make its full path relative
511 * to the $XDG_DATA_DIRS component in which the desktop file is installed,
512 * remove the "applications/" prefix, and turn '/' into '-'.
513 * For example /usr/share/applications/foo/bar.desktop has the desktop file ID
514 * foo-bar.desktop.
515 * If multiple files have the same desktop file ID, the first one in the
516 * $XDG_DATA_DIRS precedence order is used.
517 * For example, if $XDG_DATA_DIRS contains the default paths
518 * /usr/local/share:/usr/share, then /usr/local/share/applications/org.foo.bar.desktop
519 * and /usr/share/applications/org.foo.bar.desktop both have the same desktop file ID
520 * org.foo.bar.desktop, but only the first one will be used.
522 * --
524 * https://specifications.freedesktop.org/desktop-entry-spec/latest/ar01s07.html says:
526 * The application must name its desktop file in accordance with the naming
527 * recommendations in the introduction section (e.g. the filename must be like
528 * org.example.FooViewer.desktop). The application must have a D-Bus service
529 * activatable at the well-known name that is equal to the desktop file name
530 * with the .desktop portion removed (for our example, org.example.FooViewer).
532 * --
534 * Can it really be that no one thought about having multiple versions of the same
535 * application installed? What are we supposed to do if we want
536 * a) have desktop files installed by appimaged not interfere with desktop files
537 * provided by the system, i.e., if an application is installed in the system
538 * and the user also installs the AppImage, then both should be available to the user
539 * b) both should be D-Bus activatable
540 * c) the one installed by appimaged should have an AppImage vendor prefix to make
541 * it easy to distinguish it from system- or upstream-provided ones
544 /* FIXME: The following is most likely not correct; see the comments above.
545 * Open a GitHub issue or send a pull request if you would like to propose asolution. */
546 /* TODO: Check for consistency of the id with the AppStream file, if it exists in the AppImage */
547 gchar *partial_path;
548 partial_path = g_strdup_printf("applications/appimagekit_%s-%s", md5, desktop_filename);
549 gchar *destination;
550 destination = g_build_filename(g_get_user_data_dir(), partial_path, NULL);
552 /* When appimaged sees itself, then do nothing here */
553 if(strcmp ("appimaged.desktop", desktop_filename) == 0) {
554 return;
557 if(verbose)
558 fprintf(stderr, "install: %s\n", destination);
559 if(g_mkdir_with_parents(g_path_get_dirname(destination), 0755))
560 fprintf(stderr, "Could not create directory: %s\n", g_path_get_dirname(destination));
562 // g_key_file_save_to_file(key_file_structure, destination, NULL);
563 // g_key_file_save_to_file is too new, only since 2.40
564 /* Write config file on disk */
565 gsize length;
566 gchar *buf;
567 GIOChannel *file;
568 buf = g_key_file_to_data(key_file_structure, &length, NULL);
569 file = g_io_channel_new_file(destination, "w", NULL);
570 g_io_channel_write_chars(file, buf, length, NULL, NULL);
571 g_io_channel_shutdown(file, TRUE, NULL);
574 /* GNOME shows the icon and name on the desktop file only if it is executable */
575 chmod(destination, 0755);
578 /* Register a type 1 AppImage in the system */
579 bool appimage_type1_register_in_system(char *path, gboolean verbose)
581 fprintf(stderr, "ISO9660 based type 1 AppImage\n");
582 gchar *desktop_icon_value_original = NULL;
583 char *md5 = get_md5(path);
585 if(verbose)
586 fprintf(stderr, "md5 of URI RFC 2396: %s\n", md5);
588 struct archive *a;
589 struct archive_entry *entry;
590 int r;
592 a = archive_read_new();
593 archive_read_support_format_iso9660(a);
594 if ((r = archive_read_open_filename(a, path, 10240))) {
595 fprintf(stderr, "%s", archive_error_string(a));
597 for (;;) {
598 r = archive_read_next_header(a, &entry);
599 if (r == ARCHIVE_EOF) {
600 break;
602 if (r != ARCHIVE_OK) {
603 fprintf(stderr, "%s\n", archive_error_string(a));
604 break;
607 /* Skip all but regular files; FIXME: Also handle symlinks correctly */
608 if(archive_entry_filetype(entry) != AE_IFREG) {
609 continue;
611 gchar *filename;
612 filename = replace_str(archive_entry_pathname(entry), "./", "");
614 /* Get desktop file(s) in the root directory of the AppImage and act on it in one go */
615 if((g_str_has_suffix(filename,".desktop") && (NULL == strstr (filename,"/"))))
617 fprintf(stderr, "Got root desktop: %s\n", filename);
618 int r;
619 const void *buff;
620 size_t size = 1024*1024;
621 int64_t offset = 0;
622 r = archive_read_data_block(a, &buff, &size, &offset);
623 if (r == ARCHIVE_EOF)
624 return (ARCHIVE_OK);
625 if (r != ARCHIVE_OK) {
626 fprintf(stderr, "%s", archive_error_string(a));
627 break;
629 GKeyFile *key_file_structure = g_key_file_new(); // A structure that will hold the information from the desktop file
630 gboolean success = g_key_file_load_from_data (key_file_structure, buff, size, G_KEY_FILE_KEEP_COMMENTS | G_KEY_FILE_KEEP_TRANSLATIONS, NULL);
631 if(success){
632 gchar *desktop_filename = g_path_get_basename(filename);
633 desktop_icon_value_original = g_strdup_printf("%s", g_key_file_get_value(key_file_structure, "Desktop Entry", "Icon", NULL));
634 if(verbose)
635 fprintf(stderr, "desktop_icon_value_original: %s\n", desktop_icon_value_original);
636 write_edited_desktop_file(key_file_structure, path, desktop_filename, 1, md5, verbose);
638 g_key_file_free(key_file_structure);
641 gchar *dest = NULL;
642 gchar *dest_dirname = NULL;
643 gchar *dest_basename = NULL;
644 /* Get icon file(s) and act on them in one go */
646 if(g_str_has_prefix(filename, "usr/share/icons/") || g_str_has_prefix(filename, "usr/share/pixmaps/") || (g_str_has_prefix(filename, "usr/share/mime/") && g_str_has_suffix(filename, ".xml"))){
647 dest_dirname = g_path_get_dirname(replace_str(filename, "usr/share", g_get_user_data_dir()));
648 dest_basename = g_strdup_printf("%s_%s_%s", vendorprefix, md5, g_path_get_basename(filename));
649 dest = g_build_path("/", dest_dirname, dest_basename, NULL);
651 /* According to https://specifications.freedesktop.org/icon-theme-spec/icon-theme-spec-latest.html#install_icons
652 * share/pixmaps is ONLY searched in /usr but not in $XDG_DATA_DIRS and hence $HOME and this seems to be true at least in XFCE */
653 if(g_str_has_prefix (filename, "usr/share/pixmaps/")){
654 dest = g_build_path("/", "/tmp", dest_basename, NULL);
656 /* Some AppImages only have the icon in their root directory, so we have to get it from there */
657 if(desktop_icon_value_original){
658 if((g_str_has_prefix(filename, desktop_icon_value_original)) && (! strstr(filename, "/")) && ( (g_str_has_suffix(filename, ".png")) || (g_str_has_suffix(filename, ".xpm")) || (g_str_has_suffix(filename, ".svg")) || (g_str_has_suffix(filename, ".svgz")))){
659 dest_basename = g_strdup_printf("%s_%s_%s.%s", vendorprefix, md5, desktop_icon_value_original, get_file_extension(filename));
660 dest = g_build_path("/", "/tmp", dest_basename, NULL);
664 if(dest){
666 if(verbose)
667 fprintf(stderr, "install: %s\n", dest);
669 if(g_mkdir_with_parents(g_path_get_dirname(dest), 0755))
670 fprintf(stderr, "Could not create directory: %s\n", g_path_get_dirname(dest));
672 FILE *f;
673 f = fopen(dest, "w+");
675 if (f == NULL){
676 fprintf(stderr, "fopen error\n");
677 break;
680 int r;
681 const void *buff;
682 size_t size;
683 int64_t offset;
686 for (;;) {
687 r = archive_read_data_block(a, &buff, &size, &offset);
688 if (r == ARCHIVE_EOF)
689 break;
690 if (r != ARCHIVE_OK) {
691 fprintf(stderr, "%s\n", archive_error_string(a));
692 break;
694 fwrite(buff, 1, size, f);
697 fclose(f);
698 chmod (dest, 0644);
701 if(verbose)
702 fprintf(stderr, "Installed: %s\n", dest);
704 // If we were unsure about the size of an icon, we temporarily installed
705 // it to /tmp and now move it into the proper place
706 if(g_str_has_prefix (dest, "/tmp/")) {
707 move_icon_to_destination(dest, verbose);
711 archive_read_close(a);
712 archive_read_free(a);
713 set_executable(path, verbose);
714 return TRUE;
717 /* Register a type 2 AppImage in the system */
718 bool appimage_type2_register_in_system(char *path, gboolean verbose)
720 fprintf(stderr, "squashfs based type 2 AppImage\n");
721 long unsigned int fs_offset; // The offset at which a squashfs image is expected
722 char *md5 = get_md5(path);
723 GKeyFile *key_file_structure = g_key_file_new(); // A structure that will hold the information from the desktop file
724 gchar *desktop_icon_value_original = "iDoNotMatchARegex"; // FIXME: otherwise the regex does weird stuff in the first run
725 if(verbose)
726 fprintf(stderr, "md5 of URI RFC 2396: %s\n", md5);
727 fs_offset = get_elf_size(path);
728 if(verbose)
729 fprintf(stderr, "fs_offset: %lu\n", fs_offset);
730 sqfs fs;
731 sqfs_err err = sqfs_open_image(&fs, path, fs_offset);
732 if (err != SQFS_OK){
733 fprintf(stderr, "sqfs_open_image error: %s\n", path);
734 return FALSE;
735 } else {
736 if(verbose)
737 fprintf(stderr, "sqfs_open_image: %s\n", path);
740 /* TOOO: Change so that only one run of squash_get_matching_files is needed in total,
741 * this should hopefully improve performance */
743 /* Get desktop file(s) in the root directory of the AppImage */
744 gchar **str_array = squash_get_matching_files(&fs, "(^[^/]*?.desktop$)", desktop_icon_value_original, md5, verbose); // Only in root dir
745 // gchar **str_array = squash_get_matching_files(&fs, "(^.*?.desktop$)", md5, verbose); // Not only there
746 /* Work trough the NULL-terminated array of strings */
747 for (int i=0; str_array[i]; ++i) {
748 fprintf(stderr, "Got root desktop: %s\n", str_array[i]);
749 gboolean success = g_key_file_load_from_squash(&fs, str_array[i], key_file_structure, verbose);
750 if(success){
751 gchar *desktop_filename = g_path_get_basename(str_array[i]);
753 desktop_icon_value_original = g_strdup_printf("%s", g_key_file_get_value(key_file_structure, "Desktop Entry", "Icon", NULL));
754 if(verbose)
755 fprintf(stderr, "desktop_icon_value_original: %s\n", desktop_icon_value_original);
756 write_edited_desktop_file(key_file_structure, path, desktop_filename, 2, md5, verbose);
758 g_key_file_free(key_file_structure);
761 /* Free the NULL-terminated array of strings and its contents */
762 g_strfreev(str_array);
764 /* Get relevant file(s) */
765 gchar **str_array2 = squash_get_matching_files(&fs, "(^usr/share/(icons|pixmaps)/.*.(png|svg|svgz|xpm)$|^.DirIcon$|^usr/share/mime/packages/.*.xml$|^usr/share/appdata/.*metainfo.xml$|^[^/]*?.(png|svg|svgz|xpm)$)", desktop_icon_value_original, md5, verbose);
767 /* Free the NULL-terminated array of strings and its contents */
768 g_strfreev(str_array2);
770 /* The above also gets AppStream metainfo file(s), TODO: Check if the id matches and do something with them*/
772 set_executable(path, verbose);
774 return TRUE;
777 /* Register an AppImage in the system */
778 int appimage_register_in_system(char *path, gboolean verbose)
780 if((g_str_has_suffix(path, ".part")) || (g_str_has_suffix(path, ".tmp")) || (g_str_has_suffix(path, ".download")) || (g_str_has_suffix(path, ".zs-old")) || (g_str_has_suffix(path, ".~")))
781 return 0;
782 int type = check_appimage_type(path, verbose);
783 if(type == 1 || type == 2){
784 fprintf(stderr, "\n");
785 fprintf(stderr, "-> REGISTER %s\n", path);
787 /* TODO: Generate thumbnails.
788 * Generating proper thumbnails involves more than just copying images out of the AppImage,
789 * including checking if the thumbnail already exists and if it's valid
790 * and writing attributes into the thumbnail, see
791 * https://specifications.freedesktop.org/thumbnail-spec/thumbnail-spec-latest.html#CREATION */
792 if(verbose)
793 fprintf(stderr, "get_thumbnail_path: %s\n", get_thumbnail_path(path, "normal", verbose));
795 if(type == 1){
796 appimage_type1_register_in_system(path, verbose);
799 if(type == 2){
800 appimage_type2_register_in_system(path, verbose);
803 return 0;
806 /* Delete the thumbnail for a given file and size if it exists */
807 void delete_thumbnail(char *path, char *size, gboolean verbose)
809 gchar *thumbnail_path = get_thumbnail_path(path, size, verbose);
810 if(verbose)
811 fprintf(stderr, "get_thumbnail_path: %s\n", thumbnail_path);
812 if(g_file_test(thumbnail_path, G_FILE_TEST_IS_REGULAR)){
813 g_unlink(thumbnail_path);
814 if(verbose)
815 fprintf(stderr, "deleted: %s\n", thumbnail_path);
819 /* Recursively delete files in path and subdirectories that contain the given md5
821 void unregister_using_md5_id(const char *name, int level, char* md5, gboolean verbose)
823 DIR *dir;
824 struct dirent *entry;
826 if (!(dir = opendir(name)))
827 return;
828 if (!(entry = readdir(dir)))
829 return;
831 do {
832 if (entry->d_type == DT_DIR) {
833 char path[1024];
834 int len = snprintf(path, sizeof(path)-1, "%s/%s", name, entry->d_name);
835 path[len] = 0;
836 if (strcmp(entry->d_name, ".") == 0 || strcmp(entry->d_name, "..") == 0)
837 continue;
838 unregister_using_md5_id(path, level + 1, md5, verbose);
841 else if(strstr(entry->d_name, g_strdup_printf("%s_%s", vendorprefix, md5))) {
842 gchar *path_to_be_deleted = g_strdup_printf("%s/%s", name, entry->d_name);
843 if(g_file_test(path_to_be_deleted, G_FILE_TEST_IS_REGULAR)){
844 g_unlink(path_to_be_deleted);
845 if(verbose)
846 fprintf(stderr, "deleted: %s\n", path_to_be_deleted);
849 } while ((entry = readdir(dir)) != NULL);
850 closedir(dir);
854 /* Unregister an AppImage in the system */
855 int appimage_unregister_in_system(char *path, gboolean verbose)
857 char *md5 = get_md5(path);
859 /* The file is already gone by now, so we can't determine its type anymore */
860 fprintf(stderr, "_________________________\n");
861 fprintf(stderr, "\n");
862 fprintf(stderr, "-> UNREGISTER %s\n", path);
863 /* Could use gnome_desktop_thumbnail_factory_lookup instead of the next line */
865 /* Delete the thumbnails if they exist */
866 delete_thumbnail(path, "normal", verbose); // 128x128
867 delete_thumbnail(path, "large", verbose); // 256x256
869 unregister_using_md5_id(g_get_user_data_dir(), 0, md5, verbose);
871 return 0;