1 /**************************************************************************
3 * Copyright (c) 2004-17 Simon Peter
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
25 **************************************************************************/
27 #ident "AppImage by Simon Peter, http://appimage.org/"
33 #include <sys/types.h>
39 #include <glib/gprintf.h>
40 #include <glib/gstdio.h>
43 #include "squashfuse.h"
44 #include <squashfs_fs.h>
47 #include "getsection.h"
49 #if HAVE_LIBARCHIVE3 == 1 // CentOS
50 # include <archive3.h>
51 # include <archive_entry3.h>
52 #else // other systems
54 # include <archive_entry.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
72 fprintf(stderr
, "Could not set %s executable: %s\n", path
, strerror(errno
));
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
);
85 ptr
= g_strstr_len(retval
,-1,find
);
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
);
91 retval
= g_strdup(temp
);
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
);
105 checksum
= g_checksum_new(G_CHECKSUM_MD5
);
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
)
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
);
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
;
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
);
171 dest_dir
= g_build_path("/", g_get_user_data_dir(), "/icons/hicolor/", g_strdup_printf("%ix%i", w
, h
), "/apps", NULL
);
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
);
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
)
198 FILE *f
= fopen(path
, "rt");
201 /* Check magic bytes at offset 8 */
202 fseek(f
, 8, SEEK_SET
);
203 fread(buffer
, 1, 3, f
);
205 if((buffer
[0] == 0x41) && (buffer
[1] == 0x49) && (buffer
[2] == 0x01)){
206 fprintf(stderr
, "_________________________\n");
208 fprintf(stderr
, "AppImage type 1\n");
211 } else if((buffer
[0] == 0x41) && (buffer
[1] == 0x49) && (buffer
[2] == 0x02)){
212 fprintf(stderr
, "_________________________\n");
214 fprintf(stderr
, "AppImage type 2\n");
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");
225 fprintf(stderr
, "_________________________\n");
226 fprintf(stderr
, "Unrecognized file '%s'\n", path
);
235 /* Get filename extension */
236 static gchar
*get_file_extension(const gchar
*filename
)
240 tokens
= g_strsplit(filename
, ".", 2);
241 if (tokens
[0] == NULL
)
244 extension
= g_strdup(tokens
[1]);
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
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
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();
264 sqfs_err err
= sqfs_traverse_open(&trv
, fs
, sqfs_inode_root(fs
));
266 fprintf(stderr
, "sqfs_traverse_open error\n");
267 while (sqfs_traverse_next(&trv
, &err
)) {
272 regcomp(®ex
, pattern
, REG_ICASE
| REG_EXTENDED
);
273 r
= regexec(®ex
, trv
.path
, 2, match
, 0);
276 if(sqfs_inode_get(fs
, &inode
, trv
.entry
.inode
))
277 fprintf(stderr
, "sqfs_inode_get error\n");
281 fprintf(stderr
, "squash_get_matching_files found: %s\n", trv
.path
);
282 g_ptr_array_add(array
, g_strdup(trv
.path
));
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
);
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;
314 f
= fopen (dest
, "w+");
316 fprintf(stderr
, "fopen error\n");
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
;
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
);
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
355 gboolean
g_key_file_load_from_squash(sqfs
*fs
, char *path
, GKeyFile
*key_file_structure
, gboolean verbose
) {
357 gboolean success
= true;
358 sqfs_err err
= sqfs_traverse_open(&trv
, fs
, sqfs_inode_root(fs
));
360 fprintf(stderr
, "sqfs_traverse_open error\n");
361 while (sqfs_traverse_next(&trv
, &err
)) {
363 if (strcmp(path
, trv
.path
) == 0){
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
);
376 fprintf(stderr
, "TODO: Implement inode.base.inode_type %i\n", inode
.base
.inode_type
);
384 fprintf(stderr
, "sqfs_traverse_next error\n");
385 sqfs_traverse_close(&trv
);
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");
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")){
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
);
437 FILE *binary
= fopen(appimage_path
, "rt");
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
);
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");
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.
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
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.
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.
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
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.
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).
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 */
548 partial_path
= g_strdup_printf("applications/appimagekit_%s-%s", md5
, desktop_filename
);
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) {
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 */
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
);
586 fprintf(stderr
, "md5 of URI RFC 2396: %s\n", md5
);
589 struct archive_entry
*entry
;
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
));
598 r
= archive_read_next_header(a
, &entry
);
599 if (r
== ARCHIVE_EOF
) {
602 if (r
!= ARCHIVE_OK
) {
603 fprintf(stderr
, "%s\n", archive_error_string(a
));
607 /* Skip all but regular files; FIXME: Also handle symlinks correctly */
608 if(archive_entry_filetype(entry
) != AE_IFREG
) {
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
);
620 size_t size
= 1024*1024;
622 r
= archive_read_data_block(a
, &buff
, &size
, &offset
);
623 if (r
== ARCHIVE_EOF
)
625 if (r
!= ARCHIVE_OK
) {
626 fprintf(stderr
, "%s", archive_error_string(a
));
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
);
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
));
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
);
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
);
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
));
673 f
= fopen(dest
, "w+");
676 fprintf(stderr
, "fopen error\n");
687 r
= archive_read_data_block(a
, &buff
, &size
, &offset
);
688 if (r
== ARCHIVE_EOF
)
690 if (r
!= ARCHIVE_OK
) {
691 fprintf(stderr
, "%s\n", archive_error_string(a
));
694 fwrite(buff
, 1, size
, f
);
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_finish(a
);
713 set_executable(path
, verbose
);
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
726 fprintf(stderr
, "md5 of URI RFC 2396: %s\n", md5
);
727 fs_offset
= get_elf_size(path
);
729 fprintf(stderr
, "fs_offset: %lu\n", fs_offset
);
731 sqfs_err err
= sqfs_open_image(&fs
, path
, fs_offset
);
733 fprintf(stderr
, "sqfs_open_image error: %s\n", path
);
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
);
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
));
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
);
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
, ".~")))
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 */
793 fprintf(stderr
, "get_thumbnail_path: %s\n", get_thumbnail_path(path
, "normal", verbose
));
796 appimage_type1_register_in_system(path
, verbose
);
800 appimage_type2_register_in_system(path
, verbose
);
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
);
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
);
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
)
824 struct dirent
*entry
;
826 if (!(dir
= opendir(name
)))
828 if (!(entry
= readdir(dir
)))
832 if (entry
->d_type
== DT_DIR
) {
834 int len
= snprintf(path
, sizeof(path
)-1, "%s/%s", name
, entry
->d_name
);
836 if (strcmp(entry
->d_name
, ".") == 0 || strcmp(entry
->d_name
, "..") == 0)
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
);
846 fprintf(stderr
, "deleted: %s\n", path_to_be_deleted
);
849 } while ((entry
= readdir(dir
)) != NULL
);
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
);