Permalink
Fetching contributors…
Cannot retrieve contributors at this time
1813 lines (1459 sloc) 63.5 KB
/**************************************************************************
*
* Copyright (c) 2004-18 Simon Peter
*
* All Rights Reserved.
*
* Permission is hereby granted, free of charge, to any person obtaining a copy
* of this software and associated documentation files (the "Software"), to deal
* in the Software without restriction, including without limitation the rights
* to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
* copies of the Software, and to permit persons to whom the Software is
* furnished to do so, subject to the following conditions:
*
* The above copyright notice and this permission notice shall be included in
* all copies or substantial portions of the Software.
*
* THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
* IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
* FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
* AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
* LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
* OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
* THE SOFTWARE.
*
**************************************************************************/
#ident "AppImage by Simon Peter, http://appimage.org/"
#include <stdio.h>
#include <string.h>
#include <stdlib.h>
#include <unistd.h>
#include <fcntl.h>
#include <sys/stat.h>
#include <sys/types.h>
#include <dirent.h>
#include <errno.h>
#include <glib.h>
#include <glib/gprintf.h>
#include <glib/gstdio.h>
#include <gio/gio.h>
#include "squashfuse.h"
#include <squashfs_fs.h>
#include "getsection.h"
#include "elf.h"
// own header
#include "shared.h"
#if HAVE_LIBARCHIVE3 == 1 // CentOS
# include <archive3.h>
# include <archive_entry3.h>
#else // other systems
# include <archive.h>
# include <archive_entry.h>
#endif
#include <regex.h>
#include <cairo.h> // To get the size of icons, move_icon_to_destination()
#define FNM_FILE_NAME 2
#define URI_MAX (FILE_MAX * 3 + 8)
char *vendorprefix = "appimagekit";
void set_executable(const char *path, gboolean verbose)
{
if(!g_find_program_in_path ("firejail")){
int result = chmod(path, 0755); // TODO: Only do this if signature verification passed
if(result != 0){
fprintf(stderr, "Could not set %s executable: %s\n", path, strerror(errno));
} else {
if(verbose)
fprintf(stderr, "Set %s executable\n", path);
}
}
}
/* Search and replace on a string, this really should be in Glib */
gchar* replace_str(const gchar const *src, const gchar const *find, const gchar const *replace){
gchar **split = g_strsplit(src, find, -1);
gchar *retval = g_strjoinv(replace, split);
g_strfreev(split);
return retval;
}
/* Return the md5 hash constructed according to
* https://specifications.freedesktop.org/thumbnail-spec/thumbnail-spec-latest.html#THUMBSAVE
* This can be used to identify files that are related to a given AppImage at a given location */
char *appimage_get_md5(const char* path)
{
char* absolute_path;
if ((absolute_path = realpath(path, NULL)) == NULL)
absolute_path = strdup(path);
gchar *uri = g_filename_to_uri(absolute_path, NULL, NULL);
free(absolute_path);
if (uri != NULL)
{
GChecksum *checksum;
checksum = g_checksum_new(G_CHECKSUM_MD5);
guint8 digest[16];
gsize digest_len = sizeof (digest);
g_checksum_update(checksum, (const guchar *) uri, strlen (uri));
g_checksum_get_digest(checksum, digest, &digest_len);
g_assert(digest_len == 16);
gchar *result = g_strdup(g_checksum_get_string(checksum));
g_checksum_free(checksum);
g_free(uri);
return result;
} else {
return "";
}
}
/* Return the path of the thumbnail regardless whether it already exists; may be useful because
* G*_FILE_ATTRIBUTE_THUMBNAIL_PATH only exists if the thumbnail is already there.
* Check libgnomeui/gnome-thumbnail.h for actually generating thumbnails in the correct
* sizes at the correct locations automatically; which would draw in a dependency on gdk-pixbuf.
*/
char *get_thumbnail_path(const char *path, char *thumbnail_size, gboolean verbose)
{
char *md5 = appimage_get_md5(path);
char *file = g_strconcat(md5, ".png", NULL);
gchar *thumbnail_path = g_build_filename(g_get_user_cache_dir(), "thumbnails", thumbnail_size, file, NULL);
g_free(file);
g_free(md5);
return thumbnail_path;
}
/* Move an icon file to the path where a given icon can be installed in $HOME.
* This is needed because png and xpm icons cannot be installed in a generic
* location but are only picked up in directories that have the size of
* the icon as part of their directory name, as specified in the theme.index
* See https://github.com/AppImage/AppImageKit/issues/258
*/
void move_icon_to_destination(gchar *icon_path, gboolean verbose)
{
// FIXME: This default location is most likely wrong, but at least the icons with unknown size can go somewhere
gchar *dest_dir = g_build_path("/", g_get_user_data_dir(), "/icons/hicolor/48x48/apps/", NULL);
if((g_str_has_suffix (icon_path, ".svg")) || (g_str_has_suffix (icon_path, ".svgz"))) {
g_free(dest_dir);
dest_dir = g_build_path("/", g_get_user_data_dir(), "/icons/hicolor/scalable/apps/", NULL);
}
if((g_str_has_suffix (icon_path, ".png")) || (g_str_has_suffix (icon_path, ".xpm"))) {
cairo_surface_t *image;
int w = 0;
int h = 0;
if(g_str_has_suffix (icon_path, ".xpm")) {
// TODO: GdkPixbuf has a convenient way to load XPM data. Then you can call
// gdk_cairo_set_source_pixbuf() to transfer the data to a Cairo surface.
fprintf(stderr, "XPM size parsing not yet implemented\n");
}
if(g_str_has_suffix (icon_path, ".png")) {
image = cairo_image_surface_create_from_png(icon_path);
w = cairo_image_surface_get_width (image);
h = cairo_image_surface_get_height (image);
cairo_surface_destroy (image);
}
// FIXME: The following sizes are taken from the hicolor icon theme.
// Probably the right thing to do would be to figure out at runtime which icon sizes are allowable.
// Or could we put our own index.theme into .local/share/icons/ and have it observed?
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))){
fprintf(stderr, "%s has nonstandard size w = %i, h = %i; please fix it\n", icon_path, w, h);
} else {
g_free(dest_dir);
dest_dir = g_build_path("/", g_get_user_data_dir(), "/icons/hicolor/", g_strdup_printf("%ix%i", w, h), "/apps/", NULL);
}
}
if(verbose)
fprintf(stderr, "dest_dir %s\n", dest_dir);
gchar *basename = g_path_get_basename(icon_path);
gchar* icon_dest_path = g_build_path("/", dest_dir, basename, NULL);
g_free(basename);
if(verbose)
fprintf(stderr, "Move from %s to %s\n", icon_path, icon_dest_path);
gchar *dirname = g_path_get_dirname(dest_dir);
if(g_mkdir_with_parents(dirname, 0755))
fprintf(stderr, "Could not create directory: %s\n", dest_dir);
g_free(dirname);
g_free(dest_dir);
// This is required only for old versions of glib2 and is harmless for newer
g_type_init();
GError *error = NULL;
GFile *icon_file = g_file_new_for_path(icon_path);
GFile *target_file = g_file_new_for_path(icon_dest_path);
if(!g_file_move(icon_file, target_file, G_FILE_COPY_OVERWRITE, NULL, NULL, NULL, &error)){
fprintf(stderr, "Error moving file: %s\n", error->message);
g_error_free(error);
}
g_object_unref(icon_file);
g_object_unref(target_file);
g_free(icon_dest_path);
}
/* Check if a file is an AppImage. Returns the image type if it is, or -1 if it isn't */
int appimage_get_type(const char* path, gboolean verbose)
{
FILE *f = fopen(path, "rt");
if (f != NULL)
{
char buffer[3] = {0};
/* Check magic bytes at offset 8 */
fseek(f, 8, SEEK_SET);
fread(buffer, 1, 3, f);
fclose(f);
if((buffer[0] == 0x41) && (buffer[1] == 0x49) && (buffer[2] == 0x01)){
fprintf(stderr, "_________________________\n");
if(verbose){
fprintf(stderr, "AppImage type 1\n");
}
return 1;
} else if((buffer[0] == 0x41) && (buffer[1] == 0x49) && (buffer[2] == 0x02)){
fprintf(stderr, "_________________________\n");
if(verbose){
fprintf(stderr, "AppImage type 2\n");
}
return 2;
} else {
if((g_str_has_suffix(path,".AppImage")) || (g_str_has_suffix(path,".appimage"))) {
fprintf(stderr, "_________________________\n");
fprintf(stderr, "Blindly assuming AppImage type 1\n");
fprintf(stderr, "The author of this AppImage should embed the magic bytes, see https://github.com/AppImage/AppImageSpec\n");
return 1;
} else {
if(verbose){
fprintf(stderr, "_________________________\n");
fprintf(stderr, "Unrecognized file '%s'\n", path);
}
return -1;
}
}
}
return -1;
}
/* Get filename extension */
static gchar *get_file_extension(const gchar *filename)
{
gchar **tokens;
gchar *extension;
tokens = g_strsplit(filename, ".", 2);
if (tokens[0] == NULL)
extension = NULL;
else
extension = g_strdup(tokens[1]);
g_strfreev(tokens);
return extension;
}
// Read the file in chunks
void squash_extract_inode_to_file(sqfs *fs, sqfs_inode *inode, const gchar *dest) {
off_t bytes_already_read = 0;
sqfs_off_t bytes_at_a_time = 64*1024;
FILE * f;
f = fopen (dest, "w+");
if (f == NULL){
fprintf(stderr, "fopen error\n");
return;
}
while (bytes_already_read < (*inode).xtra.reg.file_size)
{
char buf[bytes_at_a_time];
if (sqfs_read_range(fs, inode, (sqfs_off_t) bytes_already_read, &bytes_at_a_time, buf))
fprintf(stderr, "sqfs_read_range error\n");
fwrite(buf, 1, (size_t) bytes_at_a_time, f);
bytes_already_read = bytes_already_read + bytes_at_a_time;
}
fclose(f);
}
/* Find files in the squashfs matching to the regex pattern.
* Returns a newly-allocated NULL-terminated array of strings.
* Use g_strfreev() to free it.
*
* The following is done within the sqfs_traverse run for performance reaons:
* 1.) For found files that are in usr/share/icons, install those icons into the system
* with a custom name that involves the md5 identifier to tie them to a particular
* AppImage.
* 2.) For found files that are in usr/share/mime/packages, install those icons into the system
* with a custom name that involves the md5 identifier to tie them to a particular
* AppImage.
*/
gchar **squash_get_matching_files_install_icons_and_mime_data(sqfs* fs, char* pattern,
gchar* desktop_icon_value_original, char* md5,
gboolean verbose) {
GPtrArray *array = g_ptr_array_new();
sqfs_traverse trv;
sqfs_err err = sqfs_traverse_open(&trv, fs, sqfs_inode_root(fs));
if (err!= SQFS_OK)
fprintf(stderr, "sqfs_traverse_open error\n");
while (sqfs_traverse_next(&trv, &err)) {
if (!trv.dir_end) {
int r;
regex_t regex;
regmatch_t match[2];
regcomp(&regex, pattern, REG_ICASE | REG_EXTENDED);
r = regexec(&regex, trv.path, 2, match, 0);
regfree(&regex);
sqfs_inode inode;
if(sqfs_inode_get(fs, &inode, trv.entry.inode))
fprintf(stderr, "sqfs_inode_get error\n");
if(r == 0){
// We have a match
if(verbose)
fprintf(stderr, "squash_get_matching_files found: %s\n", trv.path);
g_ptr_array_add(array, g_strdup(trv.path));
gchar *dest = NULL;
if(inode.base.inode_type == SQUASHFS_REG_TYPE) {
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"))){
gchar *path = replace_str(trv.path, "usr/share", g_get_user_data_dir());
char *dest_dirname = g_path_get_dirname(path);
g_free(path);
gchar *base_name = g_path_get_basename(trv.path);
gchar *dest_basename = g_strdup_printf("%s_%s_%s", vendorprefix, md5, base_name);
dest = g_build_path("/", dest_dirname, dest_basename, NULL);
g_free(base_name);
g_free(dest_basename);
}
/* According to https://specifications.freedesktop.org/icon-theme-spec/icon-theme-spec-latest.html#install_icons
* 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 */
if(g_str_has_prefix (trv.path, "usr/share/pixmaps/")){
gchar *dest_basename = g_strdup_printf("%s_%s_%s", vendorprefix, md5, g_path_get_basename(trv.path));
dest = g_build_path("/", "/tmp", dest_basename, NULL);
g_free(dest_basename);
}
/* Some AppImages only have the icon in their root directory, so we have to get it from there */
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")))){
gchar *ext = get_file_extension(trv.path);
gchar *dest_basename = g_strdup_printf("%s_%s_%s.%s", vendorprefix, md5, desktop_icon_value_original, ext);
dest = g_build_path("/", "/tmp", dest_basename, NULL);
g_free(dest_basename);
g_free(ext);
}
if(dest){
if(verbose)
fprintf(stderr, "install: %s\n", dest);
gchar *dirname = g_path_get_dirname(dest);
if(g_mkdir_with_parents(dirname, 0755))
fprintf(stderr, "Could not create directory: %s\n", dirname);
g_free(dirname);
squash_extract_inode_to_file(fs, &inode, dest);
chmod (dest, 0644);
if(verbose)
fprintf(stderr, "Installed: %s\n", dest);
// If we were unsure about the size of an icon, we temporarily installed
// it to /tmp and now move it into the proper place
if(g_str_has_prefix (dest, "/tmp/")) {
move_icon_to_destination(dest, verbose);
}
g_free(dest);
}
}
}
}
}
g_ptr_array_add(array, NULL);
if (err)
fprintf(stderr, "sqfs_traverse_next error\n");
sqfs_traverse_close(&trv);
return (gchar **) g_ptr_array_free(array, FALSE);
}
/* Loads a desktop file from squashfs into an empty GKeyFile structure.
* FIXME: Use sqfs_lookup_path() instead of g_key_file_load_from_squash()
* should help for performance. Please submit a pull request if you can
* get it to work.
*/
gboolean g_key_file_load_from_squash(sqfs *fs, char *path, GKeyFile *key_file_structure, gboolean verbose) {
sqfs_traverse trv;
gboolean success = true;
sqfs_err err = sqfs_traverse_open(&trv, fs, sqfs_inode_root(fs));
if (err != SQFS_OK)
fprintf(stderr, "sqfs_traverse_open error\n");
while (sqfs_traverse_next(&trv, &err)) {
if (!trv.dir_end) {
if (strcmp(path, trv.path) == 0){
sqfs_inode inode;
if (sqfs_inode_get(fs, &inode, trv.entry.inode))
fprintf(stderr, "sqfs_inode_get error\n");
if (inode.base.inode_type == SQUASHFS_REG_TYPE){
off_t bytes_already_read = 0;
sqfs_off_t max_bytes_to_read = 256*1024;
char buf[max_bytes_to_read];
if (sqfs_read_range(fs, &inode, (sqfs_off_t) bytes_already_read, &max_bytes_to_read, buf))
fprintf(stderr, "sqfs_read_range error\n");
// fwrite(buf, 1, max_bytes_to_read, stdout);
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);
} else {
fprintf(stderr, "TODO: Implement inode.base.inode_type %i\n", inode.base.inode_type);
}
break;
}
}
}
if (err)
fprintf(stderr, "sqfs_traverse_next error\n");
sqfs_traverse_close(&trv);
return success;
}
gchar *build_installed_desktop_file_path(gchar* md5, gchar* desktop_filename) {
gchar *partial_path;
partial_path = g_strdup_printf("applications/appimagekit_%s-%s", md5, desktop_filename);
gchar *destination;
destination = g_build_filename(g_get_user_data_dir(), partial_path, NULL);
g_free(partial_path);
return destination;
}
/* Write a modified desktop file to disk that points to the AppImage */
bool write_edited_desktop_file(GKeyFile *key_file_structure, const char* appimage_path, gchar* desktop_filename, int appimage_type, char *md5, gboolean verbose){
if(!g_key_file_has_key(key_file_structure, G_KEY_FILE_DESKTOP_GROUP, G_KEY_FILE_DESKTOP_KEY_EXEC, NULL)){
fprintf(stderr, "Desktop file has no Exec key\n");
return false;
}
// parse [Try]Exec= value, replace executable by AppImage path, append parameters
// TODO: should respect quoted strings within value
const gchar const* fields[] = {G_KEY_FILE_DESKTOP_KEY_EXEC, G_KEY_FILE_DESKTOP_KEY_TRY_EXEC};
for (int i = 0; i < (sizeof(fields) / sizeof(gchar*)); i++) {
char* field_value = g_key_file_get_value(key_file_structure, G_KEY_FILE_DESKTOP_GROUP, fields[i], NULL);
// TryExec is not a mandatory field
if (field_value == NULL) {
if (fields[i] != G_KEY_FILE_DESKTOP_KEY_EXEC)
continue;
fprintf(stderr, "%s entry missing in Desktop file\n", fields[i]);
return false;
};
// saving a copy for later free() call
char* original_field_value = field_value;
char* executable = strsep(&field_value, " ");
// error handling
if (executable == NULL) {
fprintf(stderr, "Invalid value for Exec= entry in Desktop file\n");
return false;
}
unsigned long new_exec_value_size = strlen(appimage_path) + 1;
if (field_value != NULL)
new_exec_value_size += strlen(field_value) + 1;
gchar* new_exec_value = calloc(new_exec_value_size, sizeof(gchar));
// build new value
strcpy(new_exec_value, appimage_path);
if (field_value != NULL && strlen(field_value) > 0) {
strcat(new_exec_value, " ");
strcat(new_exec_value, field_value);
}
if (original_field_value != NULL)
free(original_field_value);
g_key_file_set_value(key_file_structure, G_KEY_FILE_DESKTOP_GROUP, fields[i], new_exec_value);
g_free(new_exec_value);
}
/* If firejail is on the $PATH, then use it to run AppImages */
if(g_find_program_in_path ("firejail")){
char *firejail_exec;
firejail_exec = g_strdup_printf("firejail --env=DESKTOPINTEGRATION=appimaged --noprofile --appimage '%s'", appimage_path);
g_key_file_set_value(key_file_structure, G_KEY_FILE_DESKTOP_GROUP, G_KEY_FILE_DESKTOP_KEY_EXEC, firejail_exec);
gchar *firejail_profile_group = "Desktop Action FirejailProfile";
gchar *firejail_profile_exec = g_strdup_printf("firejail --env=DESKTOPINTEGRATION=appimaged --private --appimage '%s'", appimage_path);
gchar *firejail_tryexec = "firejail";
g_key_file_set_value(key_file_structure, firejail_profile_group, G_KEY_FILE_DESKTOP_KEY_NAME, "Run without sandbox profile");
g_key_file_set_value(key_file_structure, firejail_profile_group, G_KEY_FILE_DESKTOP_KEY_EXEC, firejail_profile_exec);
g_key_file_set_value(key_file_structure, firejail_profile_group, G_KEY_FILE_DESKTOP_KEY_TRY_EXEC, firejail_tryexec);
g_key_file_set_value(key_file_structure, G_KEY_FILE_DESKTOP_GROUP, "Actions", "FirejailProfile;");
}
/* Add AppImageUpdate desktop action
* https://specifications.freedesktop.org/desktop-entry-spec/latest/ar01s10.html
* This will only work if AppImageUpdate is on the user's $PATH.
* TODO: we could have it call this appimaged instance instead of AppImageUpdate and let it
* figure out how to update the AppImage */
unsigned long upd_offset = 0;
unsigned long upd_length = 0;
if(g_find_program_in_path ("AppImageUpdate")){
if(appimage_type == 2){
get_elf_section_offset_and_length(appimage_path, ".upd_info", &upd_offset, &upd_length);
if(upd_length != 1024)
fprintf(stderr, "WARNING: .upd_info length is %lu rather than 1024, this might be a bug in the AppImage\n", upd_length);
}
if(appimage_type == 1){
/* If we have a type 1 AppImage, then we hardcode the offset and length */
upd_offset = 33651; // ISO 9660 Volume Descriptor field
upd_length = 512; // Might be wrong
}
fprintf(stderr, ".upd_info offset: %lu\n", upd_offset);
fprintf(stderr, ".upd_info length: %lu\n", upd_length);
char buffer[3];
FILE *binary = fopen(appimage_path, "rt");
if (binary != NULL)
{
/* Check whether the first three bytes at the offset are not NULL */
fseek(binary, upd_offset, SEEK_SET);
fread(buffer, 1, 3, binary);
fclose(binary);
if((buffer[0] != 0x00) && (buffer[1] != 0x00) && (buffer[2] != 0x00)){
gchar *appimageupdate_group = "Desktop Action AppImageUpdate";
gchar *appimageupdate_exec = g_strdup_printf("%s %s", "AppImageUpdate", appimage_path);
g_key_file_set_value(key_file_structure, appimageupdate_group, G_KEY_FILE_DESKTOP_KEY_NAME, "Update");
g_key_file_set_value(key_file_structure, appimageupdate_group, G_KEY_FILE_DESKTOP_KEY_EXEC, appimageupdate_exec);
g_key_file_set_value(key_file_structure, G_KEY_FILE_DESKTOP_GROUP, "Actions", "AppImageUpdate;");
}
}
}
gchar *icon_path = g_key_file_get_value(key_file_structure, "Desktop Entry", "Icon", NULL);
gchar *basename = g_path_get_basename(icon_path);
{
gchar *icon_with_md5 = g_strdup_printf("%s_%s_%s", vendorprefix, md5, basename);
g_free(basename);
g_free(icon_path);
g_key_file_set_value(key_file_structure, "Desktop Entry", "Icon", icon_with_md5);
g_free(icon_with_md5);
}
/* At compile time, inject GIT_COMMIT like this:
* cc ... -DGIT_COMMIT=\"$(git describe --tags --always --abbrev=7)\" -..
*/
{
gchar *generated_by = g_strdup_printf("Generated by appimaged %s", GIT_COMMIT);
g_key_file_set_value(key_file_structure, "Desktop Entry", "X-AppImage-Comment", generated_by);
g_free(generated_by);
}
g_key_file_set_value(key_file_structure, "Desktop Entry", "X-AppImage-Identifier", md5);
fprintf(stderr, "Installing desktop file\n");
if(verbose) {
gchar *buf = g_key_file_to_data(key_file_structure, NULL, NULL);
fprintf(stderr, "%s", buf);
g_free(buf);
}
/* https://specifications.freedesktop.org/menu-spec/menu-spec-latest.html#paths says:
*
* $XDG_DATA_DIRS/applications/
* When two desktop entries have the same name, the one appearing earlier in the path is used.
*
* --
*
* https://developer.gnome.org/integration-guide/stable/desktop-files.html.en says:
*
* Place this file in the /usr/share/applications directory so that it is accessible
* by everyone, or in ~/.local/share/applications if you only wish to make it accessible
* to a single user. Which is used should depend on whether your application is
* installed systemwide or into a user's home directory. GNOME monitors these directories
* for changes, so simply copying the file to the right location is enough to register it
* with the desktop.
*
* Note that the ~/.local/share/applications location is not monitored by versions of GNOME
* prior to version 2.10 or on Fedora Core Linux, prior to version 2.8.
* These versions of GNOME follow the now-deprecated vfolder standard,
* and so desktop files must be installed to ~/.gnome2/vfolders/applications.
* This location is not supported by GNOME 2.8 on Fedora Core nor on upstream GNOME 2.10
* so for maximum compatibility with deployed desktops, put the file in both locations.
*
* Note that the KDE Desktop requires one to run kbuildsycoca to force a refresh of the menus.
*
* --
*
* https://specifications.freedesktop.org/menu-spec/menu-spec-latest.html says:
*
* To prevent that a desktop entry from one party inadvertently cancels out
* the desktop entry from another party because both happen to get the same
* desktop-file id it is recommended that providers of desktop-files ensure
* that all desktop-file ids start with a vendor prefix.
* A vendor prefix consists of [a-zA-Z] and is terminated with a dash ("-").
* For example, to ensure that GNOME applications start with a vendor prefix of "gnome-",
* it could either add "gnome-" to all the desktop files it installs
* in datadir/applications/ or it could install desktop files in a
* datadir/applications/gnome subdirectory.
*
* --
*
* https://specifications.freedesktop.org/desktop-entry-spec/latest/ape.html says:
* The desktop file ID is the identifier of an installed desktop entry file.
*
* To determine the ID of a desktop file, make its full path relative
* to the $XDG_DATA_DIRS component in which the desktop file is installed,
* remove the "applications/" prefix, and turn '/' into '-'.
* For example /usr/share/applications/foo/bar.desktop has the desktop file ID
* foo-bar.desktop.
* If multiple files have the same desktop file ID, the first one in the
* $XDG_DATA_DIRS precedence order is used.
* For example, if $XDG_DATA_DIRS contains the default paths
* /usr/local/share:/usr/share, then /usr/local/share/applications/org.foo.bar.desktop
* and /usr/share/applications/org.foo.bar.desktop both have the same desktop file ID
* org.foo.bar.desktop, but only the first one will be used.
*
* --
*
* https://specifications.freedesktop.org/desktop-entry-spec/latest/ar01s07.html says:
*
* The application must name its desktop file in accordance with the naming
* recommendations in the introduction section (e.g. the filename must be like
* org.example.FooViewer.desktop). The application must have a D-Bus service
* activatable at the well-known name that is equal to the desktop file name
* with the .desktop portion removed (for our example, org.example.FooViewer).
*
* --
*
* Can it really be that no one thought about having multiple versions of the same
* application installed? What are we supposed to do if we want
* a) have desktop files installed by appimaged not interfere with desktop files
* provided by the system, i.e., if an application is installed in the system
* and the user also installs the AppImage, then both should be available to the user
* b) both should be D-Bus activatable
* c) the one installed by appimaged should have an AppImage vendor prefix to make
* it easy to distinguish it from system- or upstream-provided ones
*/
/* FIXME: The following is most likely not correct; see the comments above.
* Open a GitHub issue or send a pull request if you would like to propose asolution. */
/* TODO: Check for consistency of the id with the AppStream file, if it exists in the AppImage */
gchar *destination = build_installed_desktop_file_path(md5, desktop_filename);
/* When appimaged sees itself, then do nothing here */
if(strcmp ("appimaged.desktop", desktop_filename) == 0) {
g_free(destination);
fprintf(stderr, "appimaged's desktop file found -- not installing desktop file for myself\n");
return true;
}
if(verbose)
fprintf(stderr, "install: %s\n", destination);
gchar *dirname = g_path_get_dirname(destination);
if(g_mkdir_with_parents(dirname, 0755))
fprintf(stderr, "Could not create directory: %s\n", dirname);
g_free(dirname);
// g_key_file_save_to_file(key_file_structure, destination, NULL);
// g_key_file_save_to_file is too new, only since 2.40
/* Write config file on disk */
gsize length;
gchar *buf;
GIOChannel *file;
buf = g_key_file_to_data(key_file_structure, &length, NULL);
file = g_io_channel_new_file(destination, "w", NULL);
g_io_channel_write_chars(file, buf, length, NULL, NULL);
g_io_channel_shutdown(file, TRUE, NULL);
g_io_channel_unref(file);
g_free(buf);
/* GNOME shows the icon and name on the desktop file only if it is executable */
chmod(destination, 0755);
g_free(destination);
return true;
}
bool appimage_type1_get_desktop_filename_and_key_file(struct archive** a, gchar** desktop_filename, GKeyFile** key_file) {
// iterate over all files ("entries") in the archive
// looking for a file with .desktop extension in the root directory
// must not be freed
struct archive_entry* entry;
gchar* filename;
for (;;) {
int r = archive_read_next_header(*a, &entry);
if (r == ARCHIVE_EOF) {
return false;
}
if (r != ARCHIVE_OK) {
fprintf(stderr, "%s\n", archive_error_string(*a));
return false;
}
/* Skip all but regular files; FIXME: Also handle symlinks correctly */
if (archive_entry_filetype(entry) != AE_IFREG)
continue;
filename = replace_str(archive_entry_pathname(entry), "./", "");
/* Get desktop file(s) in the root directory of the AppImage and act on it in one go */
if ((g_str_has_suffix(filename, ".desktop") && (NULL == strstr(filename, "/")))) {
fprintf(stderr, "Got root desktop: %s\n", filename);
const void* buff;
size_t size = 1024 * 1024;
int64_t offset = 0;
r = archive_read_data_block(*a, &buff, &size, &offset);
if (r == ARCHIVE_EOF) {
// cleanup
g_free(filename);
return true;
}
if (r != ARCHIVE_OK) {
fprintf(stderr, "%s", archive_error_string(*a));
break;
}
*desktop_filename = g_strdup(g_path_get_basename(filename));
// a structure that will hold the information from the desktop file
*key_file = g_key_file_new();
gboolean success = g_key_file_load_from_data(*key_file, buff, size,
G_KEY_FILE_KEEP_COMMENTS | G_KEY_FILE_KEEP_TRANSLATIONS, NULL);
if (!success) {
// cleanup
g_free(key_file);
key_file = NULL;
break;
}
// cleanup
g_free(filename);
return true;
}
}
g_free(filename);
return false;
}
bool archive_copy_icons_recursively_to_destination(struct archive** a, const gchar* md5,
const gchar* desktop_icon_value_original, gboolean verbose) {
// iterate over all files ("entries") in the archive
// looking for a file with .desktop extension in the root directory
struct archive_entry* entry;
gchar* filename = NULL;
bool errored = false;
for (;;) {
int r = archive_read_next_header(*a, &entry);
if (r == ARCHIVE_EOF) {
break;
}
if (r != ARCHIVE_OK) {
fprintf(stderr, "%s\n", archive_error_string(*a));
errored = true;
break;
}
/* Skip all but regular files; FIXME: Also handle symlinks correctly */
if (archive_entry_filetype(entry) != AE_IFREG)
continue;
filename = replace_str(archive_entry_pathname(entry), "./", "");
gchar* dest = NULL;
// Get icon file(s) and MIME types and act on them in one go
// add vendor prefix (and MD5 hash as an identifier for future operations)
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"))
) {
gchar* dest_path = replace_str(filename, "usr/share", g_get_user_data_dir());
gchar* dest_dirname = g_path_get_dirname(dest_path);
g_free(dest_path);
gchar* file_basename = g_path_get_basename(filename);
gchar* dest_basename = g_strdup_printf("%s_%s_%s", vendorprefix, md5, file_basename);
g_free(file_basename);
dest = g_build_path("/", dest_dirname, dest_basename, NULL);
g_free(dest_basename);
g_free(dest_dirname);
}
// According to https://specifications.freedesktop.org/icon-theme-spec/icon-theme-spec-latest.html#install_icons
// 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
if (g_str_has_prefix(filename, "usr/share/pixmaps/")) {
// clean up dest in case it has been set before
g_free(dest);
// TODO: avoid cluttering /tmp too much
dest = g_build_path("/", "/tmp", NULL);
}
if (desktop_icon_value_original != NULL) {
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"))) {
gchar* file_extension = get_file_extension(filename);
gchar* dest_basename = g_strdup_printf("%s_%s_%s.%s", vendorprefix, md5, desktop_icon_value_original,
file_extension);
g_free(file_extension);
// clean up dest in case it has been set before
g_free(dest);
// TODO: avoid cluttering /tmp too much
dest = g_build_path("/", "/tmp", dest_basename, NULL);
g_free(dest_basename);
}
}
// cleanup
g_free(filename);
if (dest != NULL) {
if (verbose)
fprintf(stderr, "install: %s\n", dest);
gchar* dir_name = g_path_get_dirname(dest);
if (g_mkdir_with_parents(dir_name, 0755)) {
fprintf(stderr, "Could not create directory: %s\n", dir_name);
// cleanup
g_free(dir_name);
errored = true;
break;
}
g_free(dir_name);
FILE *f = fopen(dest, "w+");
if (f == NULL) {
int error = errno;
fprintf(stderr, "Could not open file %s for writing: %s\n", dest, strerror(error));
// cleanup
g_free(dest);
errored = true;
break;
}
const void* buff;
size_t size;
int64_t offset;
for (;;) {
r = archive_read_data_block(*a, &buff, &size, &offset);
if (r == ARCHIVE_EOF)
break;
if (r != ARCHIVE_OK) {
fprintf(stderr, "%s\n", archive_error_string(*a));
break;
}
if ((fwrite(buff, size, 1, f) != size) || ferror(f) || feof(f)) {
int error = errno;
fprintf(stderr, "Failed to copy icon: %s\n", strerror(error));
errored = true;
break;
}
}
fclose(f);
chmod(dest, 0644);
if (verbose && !errored) {
fprintf(stderr, "Installed: %s\n", dest);
}
if (g_str_has_prefix(dest, "/tmp")) {
move_icon_to_destination(dest, verbose);
}
g_free(dest);
if (errored)
break;
}
}
return !errored;
}
/* Register a type 1 AppImage in the system */
bool appimage_type1_register_in_system(const char *path, gboolean verbose)
{
fprintf(stderr, "ISO9660 based type 1 AppImage\n");
gchar *desktop_icon_value_original = NULL;
char *md5 = appimage_get_md5(path);
if(verbose)
fprintf(stderr, "md5 of URI RFC 2396: %s\n", md5);
// open ISO9660 image using libarchive
struct archive *a = archive_read_new();
archive_read_support_format_iso9660(a);
// libarchive status int -- passed to called functions
int r;
// use global bool to not have duplicate cleanup code in the following calls
// all if() checks need to be prefixed with "!errored &&" therefore, and on error need to set errored to true
// this would be _by far_ less complex code in C++, where lambdas and strings and other nice things exist...
bool errored = false;
if ((r = archive_read_open_filename(a, path, 10240)) != ARCHIVE_OK) {
fprintf(stderr, "%s\n", archive_error_string(a));
errored = true;
}
// search image for root desktop file, and read it into key file structure so it can be edited eventually
gchar *desktop_filename = NULL;
GKeyFile *key_file = NULL;
if (!errored && !appimage_type1_get_desktop_filename_and_key_file(&a, &desktop_filename, &key_file)) {
errored = true;
}
// validate that both have been set to a non-NULL value
if (desktop_filename == NULL || key_file == NULL) {
errored = true;
}
if (!errored) {
desktop_icon_value_original = g_key_file_get_string(key_file, "Desktop Entry", "Icon", NULL);
if (verbose)
fprintf(stderr, "desktop_icon_value_original: %s\n", desktop_icon_value_original);
if (!write_edited_desktop_file(key_file, path, desktop_filename, 1, md5, verbose)) {
fprintf(stderr, "Failed to install desktop file\n");
errored = true;
}
}
// next step: copy icons recursively to their target destination
// reopen ISO9660 image
// TODO: merge both steps (reading desktop file and copying icons) again to not have to read the ISO file twice
if (!errored) {
// close and reopen archive
archive_read_free(a);
a = archive_read_new();
archive_read_support_format_iso9660(a);
if ((r = archive_read_open_filename(a, path, 10240)) != ARCHIVE_OK) {
fprintf(stderr, "archive error: %s\n", archive_error_string(a));
errored = true;
} else {
if (!archive_copy_icons_recursively_to_destination(&a, md5, desktop_icon_value_original, verbose)) {
errored = true;
}
}
}
// cleanup
g_free(desktop_filename);
g_free(desktop_icon_value_original);
g_free(md5);
g_key_file_free(key_file);
archive_read_free(a);
return !errored;
}
bool appimage_type2_get_desktop_filename_and_key_file(sqfs* fs, gchar** desktop_filename, gchar* md5, GKeyFile** key_file, gboolean verbose) {
/* TOOO: Change so that only one run of squash_get_matching_files is needed in total,
* this should hopefully improve performance */
/* Get desktop file(s) in the root directory of the AppImage */
// Only in root dir
gchar** str_array = squash_get_matching_files_install_icons_and_mime_data(fs, "(^[^/]*?.desktop$)", "", md5, verbose);
bool errored = false;
// gchar **str_array = squash_get_matching_files(&fs, "(^.*?.desktop$)", md5, verbose); // Not only there
/* Work trough the NULL-terminated array of strings */
for (int i = 0; str_array[i]; ++i) {
fprintf(stderr, "Got root desktop: %s\n", str_array[i]);
if (!g_key_file_load_from_squash(fs, str_array[i], *key_file, verbose))
errored = true;
else
*desktop_filename = g_path_get_basename(str_array[i]);
}
/* Free the NULL-terminated array of strings and its contents */
g_strfreev(str_array);
return !errored;
}
/* Register a type 2 AppImage in the system */
bool appimage_type2_register_in_system(char *path, gboolean verbose) {
fprintf(stderr, "squashfs based type 2 AppImage\n");
// the offset at which a squashfs image is expected
long unsigned int fs_offset;
char* md5 = appimage_get_md5(path);
// a structure that will hold the information from the desktop file
GKeyFile* key_file = g_key_file_new();
// FIXME: otherwise the regex does weird stuff in the first run
gchar* desktop_icon_value_original = "iDoNotMatchARegex";
if (verbose)
fprintf(stderr, "md5 of URI RFC 2396: %s\n", md5);
fs_offset = get_elf_size(path);
if (verbose)
fprintf(stderr, "fs_offset: %lu\n", fs_offset);
sqfs fs;
sqfs_err err = sqfs_open_image(&fs, path, fs_offset);
if (err != SQFS_OK) {
sqfs_destroy(&fs);
fprintf(stderr, "sqfs_open_image error: %s\n", path);
return FALSE;
} else {
if (verbose)
fprintf(stderr, "sqfs_open_image: %s\n", path);
}
gchar* desktop_filename = NULL;
bool errored = false;
if (appimage_type2_get_desktop_filename_and_key_file(&fs, &desktop_filename, md5, &key_file, verbose)) {
desktop_icon_value_original = g_key_file_get_value(key_file, "Desktop Entry", "Icon", NULL);
if (desktop_icon_value_original == NULL) {
errored = true;
} else {
if (verbose)
fprintf(stderr, "desktop_icon_value_original: %s\n", desktop_icon_value_original);
if (!write_edited_desktop_file(key_file, path, desktop_filename, 2, md5, verbose)) {
fprintf(stderr, "Failed to install desktop file\n");
return false;
}
}
} else {
errored = true;
}
if (!errored) {
/* Get relevant file(s) */
static char* const pattern = "(^usr/share/(icons|pixmaps)/.*.(png|svg|svgz|xpm)$|^.DirIcon$|^usr/share/mime/packages/.*.xml$|^usr/share/appdata/.*metainfo.xml$|^[^/]*?.(png|svg|svgz|xpm)$)";
gchar** str_array2 = squash_get_matching_files_install_icons_and_mime_data(&fs, pattern, desktop_icon_value_original, md5, verbose);
/* Free the NULL-terminated array of strings and its contents */
g_strfreev(str_array2);
}
/* The above also gets AppStream metainfo file(s), TODO: Check if the id matches and do something with them*/
set_executable(path, verbose);
g_free(desktop_filename);
sqfs_destroy(&fs);
g_free(md5);
g_free(desktop_icon_value_original);
return TRUE;
}
/* Check whether AppImage is registered in the system already */
bool appimage_is_registered_in_system(const char* path) {
// there's two criteria whether an AppImage has been registered in the system:
// 1) Has the thumbnail been created?
// 2) Has the desktop file been registered?
// if both questions can be answered yes, then an AppImage has been registered properly
if (!g_file_test(path, G_FILE_TEST_IS_REGULAR))
return false;
gchar* thumbnail_path = get_thumbnail_path(path, "normal", false);
if (!g_file_test(thumbnail_path, G_FILE_TEST_IS_REGULAR)) {
// cleanup
g_free(thumbnail_path);
return false;
}
gchar* md5 = appimage_get_md5(path);
int appimage_type = appimage_get_type(path, false);
GKeyFile* key_file = g_key_file_new();
gchar* desktop_filename = NULL;
switch (appimage_type) {
case 1:
{
// open AppImage as ISO9660 file
struct archive *a = archive_read_new();
if (archive_read_support_format_iso9660(a) != ARCHIVE_OK ||
archive_read_open_filename(a, path, 10240) != ARCHIVE_OK) {
// cleanup
archive_read_free(a);
g_free(thumbnail_path);
g_free(md5);
g_free(desktop_filename);
g_key_file_free(key_file);
return false;
}
bool rv = appimage_type1_get_desktop_filename_and_key_file(&a, &desktop_filename, &key_file);
archive_read_free(a);
if (!rv) {
// cleanup
g_free(thumbnail_path);
g_free(md5);
g_key_file_free(key_file);
g_free(desktop_filename);
return false;
}
}
break;
case 2:
{
long unsigned int fs_offset; // The offset at which a squashfs image is expected
fs_offset = get_elf_size(path);
sqfs fs;
sqfs_err err = sqfs_open_image(&fs, path, fs_offset);
if (err != SQFS_OK) {
fprintf(stderr, "sqfs_open_image error: %s\n", path);
// cleanup
sqfs_destroy(&fs);
g_free(thumbnail_path);
g_free(md5);
g_free(desktop_filename);
g_key_file_free(key_file);
return false;
}
bool rv = appimage_type2_get_desktop_filename_and_key_file(&fs, &desktop_filename, md5, &key_file, false);
sqfs_destroy(&fs);
if (!rv) {
// cleanup
g_free(thumbnail_path);
g_free(md5);
g_free(desktop_filename);
g_key_file_free(key_file);
return false;
}
}
break;
default:
// type not recognized
// cleanup
g_free(thumbnail_path);
g_free(md5);
g_key_file_free(key_file);
g_free(desktop_filename);
return false;
}
gchar* desktop_file_path = build_installed_desktop_file_path(md5, desktop_filename);
bool rv = true;
if (!g_file_test(desktop_file_path, G_FILE_TEST_IS_REGULAR))
rv = false;
g_free(thumbnail_path);
g_free(md5);
g_free(desktop_filename);
g_key_file_free(key_file);
return rv;
}
/*
* Register an AppImage in the system
* Returns 0 on success, non-0 otherwise.
*/
int appimage_register_in_system(char *path, gboolean verbose)
{
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, ".~")
) {
return 0;
}
int type = appimage_get_type(path, verbose);
if(type != -1) {
fprintf(stderr, "\n-> Registering type %d AppImage: %s\n", type, path);
appimage_create_thumbnail(path, false);
} else {
if (verbose)
fprintf(stderr, "-> Skipping file %s\n", path);
return 0;
}
switch (type) {
case 1:
appimage_type1_register_in_system(path, verbose);
break;
case 2:
appimage_type2_register_in_system(path, verbose);
break;
default:
fprintf(stderr, "Error: unknown AppImage type %d\n", type);
return 1;
}
if (type == 1) {
if (!appimage_type1_register_in_system(path, verbose))
return 1;
}
if (type == 2) {
if (!appimage_type2_register_in_system(path, verbose))
return 1;
}
return 0;
}
/* Delete the thumbnail for a given file and size if it exists */
void delete_thumbnail(char *path, char *size, gboolean verbose)
{
gchar *thumbnail_path = get_thumbnail_path(path, size, verbose);
if(verbose)
fprintf(stderr, "get_thumbnail_path: %s\n", thumbnail_path);
if(g_file_test(thumbnail_path, G_FILE_TEST_IS_REGULAR)){
g_unlink(thumbnail_path);
if(verbose)
fprintf(stderr, "deleted: %s\n", thumbnail_path);
}
g_free(thumbnail_path);
}
/* Recursively delete files in path and subdirectories that contain the given md5
*/
void unregister_using_md5_id(const char *name, int level, char* md5, gboolean verbose)
{
DIR *dir;
struct dirent *entry;
if (!(dir = opendir(name)))
return;
if (!(entry = readdir(dir)))
return;
do {
if (entry->d_type == DT_DIR) {
char path[1024];
int len = snprintf(path, sizeof(path)-1, "%s/%s", name, entry->d_name);
path[len] = 0;
if (strcmp(entry->d_name, ".") == 0 || strcmp(entry->d_name, "..") == 0)
continue;
unregister_using_md5_id(path, level + 1, md5, verbose);
}
else {
gchar *needle = g_strdup_printf("%s_%s", vendorprefix, md5);
if(strstr(entry->d_name, needle)) {
gchar *path_to_be_deleted = g_strdup_printf("%s/%s", name, entry->d_name);
if(g_file_test(path_to_be_deleted, G_FILE_TEST_IS_REGULAR)){
g_unlink(path_to_be_deleted);
if(verbose)
fprintf(stderr, "deleted: %s\n", path_to_be_deleted);
}
g_free(path_to_be_deleted);
}
g_free(needle);
}
} while ((entry = readdir(dir)) != NULL);
closedir(dir);
}
/* Unregister an AppImage in the system */
int appimage_unregister_in_system(char *path, gboolean verbose)
{
char *md5 = appimage_get_md5(path);
/* The file is already gone by now, so we can't determine its type anymore */
fprintf(stderr, "_________________________\n");
fprintf(stderr, "\n");
fprintf(stderr, "-> UNREGISTER %s\n", path);
/* Could use gnome_desktop_thumbnail_factory_lookup instead of the next line */
/* Delete the thumbnails if they exist */
delete_thumbnail(path, "normal", verbose); // 128x128
delete_thumbnail(path, "large", verbose); // 256x256
unregister_using_md5_id(g_get_user_data_dir(), 0, md5, verbose);
g_free(md5);
return 0;
}
bool is_handler_valid(const appimage_handler *handler) {
if (!handler) {
fprintf(stderr, "WARNING: Invalid handler found, you should take a look at this now!");
return false;
}
return true;
}
void mk_base_dir(const char *target)
{
gchar *dirname = g_path_get_dirname(target);
if(g_mkdir_with_parents(dirname, 0755))
fprintf(stderr, "Could not create directory: %s\n", dirname);
g_free(dirname);
}
/*
* Dummy fallback functions
*/
void dummy_traverse_func(appimage_handler *handler, traverse_cb command, void *data) {
(void) handler;
(void) command;
(void) data;
fprintf(stderr, "Called %s\n", __FUNCTION__);
}
char* dummy_get_file_name (appimage_handler *handler, void *data) {
fprintf(stderr, "Called %s\n", __FUNCTION__);
}
void dummy_extract_file(struct appimage_handler *handler, void *data, char *target) {
fprintf(stderr, "Called %s\n", __FUNCTION__);
}
/*
* AppImage Type 1 Functions
*/
void appimage_type1_open(appimage_handler *handler) {
if ( is_handler_valid(handler) && !handler->is_open ) {
fprintf(stderr, "Opening %s as Type 1 AppImage\n", handler->path);
struct archive *a;
a = archive_read_new();
archive_read_support_format_iso9660(a);
if (archive_read_open_filename(a, handler->path, 10240) != ARCHIVE_OK) {
fprintf(stderr, "%s", archive_error_string(a));
handler->cache = NULL;
handler->is_open = false;
} else {
handler->cache = a;
handler->is_open = true;
}
}
}
void appimage_type1_close(appimage_handler *handler) {
if ( is_handler_valid(handler) && handler->is_open ) {
fprintf(stderr, "Closing %s\n", handler->path);
struct archive *a = handler->cache;
archive_read_close(a);
archive_read_free(a);
handler->cache = NULL;
handler->is_open = false;
}
}
void appimage_type1_traverse(appimage_handler *handler, traverse_cb command, void *command_data) {
appimage_type1_open(handler);
if (!command) {
fprintf(stderr, "No traverse command set.\n");
return;
}
if (handler->is_open) {
struct archive *a = handler->cache;
struct archive_entry *entry;
int r;
for (;;) {
r = archive_read_next_header(a, &entry);
if (r == ARCHIVE_EOF) {
break;
}
if (r != ARCHIVE_OK) {
fprintf(stderr, "%s\n", archive_error_string(a));
break;
}
/* Skip all but regular files; FIXME: Also handle symlinks correctly */
if(archive_entry_filetype(entry) != AE_IFREG) {
continue;
}
command(handler, entry, command_data);
}
}
appimage_type1_close(handler);
}
char* appimage_type1_get_file_name (appimage_handler *handler, void *data) {
(void) handler;
struct archive_entry *entry = (struct archive_entry *) data;
char *filename = replace_str(archive_entry_pathname(entry), "./", "");
return filename;
}
void appimage_type1_extract_file (appimage_handler *handler, void *data, const char *target) {
(void) data;
struct archive *a = handler->cache;
mk_base_dir(target);
mode_t mode = S_IRUSR | S_IWUSR | S_IRGRP | S_IROTH;
int f = open(target, O_WRONLY | O_CREAT | O_TRUNC, mode);
if (f == -1){
fprintf(stderr, "open error: %s\n", target);
return;
}
archive_read_data_into_fd(a, f);
close(f);
}
appimage_handler appimage_type_1_create_handler() {
appimage_handler h;
h.traverse = appimage_type1_traverse;
h.get_file_name = appimage_type1_get_file_name;
h.extract_file = appimage_type1_extract_file;
return h;
}
/*
* AppImage Type 2 Functions
*/
void appimage_type2_open(appimage_handler *handler) {
if (is_handler_valid(handler) && !handler->is_open) {
fprintf(stderr, "Opening %s as Type 2 AppImage\n", handler->path);
long unsigned int fs_offset; // The offset at which a squashfs image is expected
fs_offset = get_elf_size(handler->path);
sqfs *fs = malloc(sizeof(sqfs));
sqfs_err err = sqfs_open_image(fs, handler->path, fs_offset);
if (err != SQFS_OK){
fprintf(stderr, "sqfs_open_image error: %s\n", handler->path);
free(fs);
handler->is_open = false;
handler->cache = NULL;
} else {
handler->is_open = true;
handler->cache = fs;
}
}
}
void appimage_type2_close(appimage_handler *handler) {
if ( is_handler_valid(handler) && handler->is_open ) {
fprintf(stderr, "Closing %s\n", handler->path);
sqfs_destroy(handler->cache);
free(handler->cache);
handler->is_open = false;
handler->cache = NULL;
}
}
void appimage_type2_traverse(appimage_handler *handler, traverse_cb command, void *command_data) {
appimage_type2_open(handler);
if (handler->is_open && handler->cache != NULL) {
sqfs *fs = handler->cache;
sqfs_traverse trv;
sqfs_inode_id root_inode = sqfs_inode_root(fs);
sqfs_err err = sqfs_traverse_open(&trv, fs, root_inode);
if (err!= SQFS_OK)
fprintf(stderr, "sqfs_traverse_open error\n");
while (sqfs_traverse_next(&trv, &err))
command(handler, &trv, command_data);
if (err)
fprintf(stderr, "sqfs_traverse_next error\n");
sqfs_traverse_close(&trv);
}
appimage_type2_close(handler);
}
char* appimage_type2_get_file_name (appimage_handler *handler, void *data) {
(void) handler;
sqfs_traverse *trv = data;
return strdup(trv->path);
}
void appimage_type2_extract_symlink(sqfs *fs, sqfs_inode *inode, const char *target);
void appimage_type2_extract_regular_file(sqfs *fs, sqfs_inode *inode, const char* target) {
mk_base_dir(target);
// Read the file in chunks
squash_extract_inode_to_file(fs, inode, target);
}
void appimage_type2_extract_file_following_symlinks(sqfs *fs, sqfs_inode *inode, const char* target) {
if(inode->base.inode_type == SQUASHFS_REG_TYPE)
appimage_type2_extract_regular_file(fs, inode, target);
else if(inode->base.inode_type == SQUASHFS_SYMLINK_TYPE) {
appimage_type2_extract_symlink(fs, inode, target);
} else
fprintf(stderr, "WARNING: Unable to extract file of type %d", inode->base.inode_type);
}
void appimage_type2_extract_symlink(sqfs *fs, sqfs_inode *inode, const char *target) {
size_t size;
sqfs_readlink(fs, inode, NULL, &size);
char buf[size];
int ret = sqfs_readlink(fs, inode, buf, &size);
if (ret != 0)
fprintf(stderr, "WARNING: Symlink error.");
else {
sqfs_err err = sqfs_inode_get(fs, inode, fs->sb.root_inode);
if (err != SQFS_OK)
fprintf(stderr, "WARNING: Unable to get the root inode. Error: %d", err);
bool found = false;
err = sqfs_lookup_path(fs, inode, buf, &found);
if (err != SQFS_OK)
fprintf(stderr, "WARNING: There was an error while trying to lookup a symblink. Error: %d", err);
if (found)
appimage_type2_extract_file_following_symlinks(fs, inode, target);
}
}
void appimage_type2_extract_file (appimage_handler *handler, void *data, const char *target) {
sqfs *fs = handler->cache;
sqfs_traverse *trv = data;
sqfs_inode inode;
if(sqfs_inode_get(fs, &inode, trv->entry.inode))
fprintf(stderr, "sqfs_inode_get error\n");
appimage_type2_extract_file_following_symlinks(fs, &inode, target);
}
appimage_handler appimage_type_2_create_handler() {
appimage_handler h;
h.traverse = appimage_type2_traverse;
h.get_file_name = appimage_type2_get_file_name;
h.extract_file = appimage_type2_extract_file;
return h;
}
/* Factory function for creating the right appimage handler for
* a given file. */
appimage_handler create_appimage_handler(const char *const path) {
int appimage_type = appimage_get_type(path, 0);
appimage_handler handler;
fprintf(stderr,"AppImage type: %d\n", appimage_type);
switch (appimage_type) {
case 1:
handler = appimage_type_1_create_handler();
break;
case 2:
handler = appimage_type_2_create_handler();
break;
default:
fprintf(stderr,"Appimage type %d not supported yet\n", appimage_type);
handler.traverse = dummy_traverse_func;
break;
}
handler.path = path;
handler.is_open = false;
return handler;
}
void move_file(const char *source, const char *target) {
g_type_init();
GError *error = NULL;
GFile *icon_file = g_file_new_for_path(source);
GFile *target_file = g_file_new_for_path(target);
if (!g_file_move (icon_file, target_file, G_FILE_COPY_OVERWRITE, NULL, NULL, NULL, &error)) {
fprintf(stderr, "Error moving file: %s\n", error->message);
g_clear_error (&error);
}
g_object_unref(icon_file);
g_object_unref(target_file);
}
struct extract_appimage_file_command_data {
const char *path;
const char *destination;
};
void extract_appimage_file_command(void *handler_data, void *entry_data, void *user_data) {
appimage_handler *h = handler_data;
struct archive_entry *entry = entry_data;
const struct extract_appimage_file_command_data const *params = user_data;
char *filename = h->get_file_name(h, entry);
if (strcmp(params->path, filename) == 0)
h->extract_file(h, entry, params->destination);
free(filename);
}
void extract_appimage_file(appimage_handler *h, const char *path, const char *destination) {
struct extract_appimage_file_command_data data;
data.path = path;
data.destination = destination;
h->traverse(h, extract_appimage_file_command, &data);
}
void extract_appimage_icon_command(void *handler_data, void *entry_data, void *user_data) {
appimage_handler *h = handler_data;
struct archive_entry *entry = entry_data;
gchar *path = user_data;
char *filename = h->get_file_name(h, entry);
if (strcmp(".DirIcon", filename) == 0)
h->extract_file(h, entry, path);
free(filename);
}
void extract_appimage_icon(appimage_handler *h, gchar *target) {
h->traverse(h, extract_appimage_icon_command, target);
}
/* Create AppImage thumbanil according to
* https://specifications.freedesktop.org/thumbnail-spec/0.8.0/index.html
*/
void appimage_create_thumbnail(const gchar *appimage_file_path, gboolean verbose) {
// extract AppImage icon to /tmp
appimage_handler handler = create_appimage_handler(appimage_file_path);
char *tmp_path = "/tmp/appimage_thumbnail_tmp";
extract_appimage_icon(&handler, tmp_path);
if (g_file_test(tmp_path, G_FILE_TEST_EXISTS) ) {
// TODO: transform it to png with sizes 128x128 and 254x254
gchar *target_path = get_thumbnail_path(appimage_file_path, "normal", verbose);
mk_base_dir(target_path);
// deploy icon as thumbnail
move_file (tmp_path, target_path);
// clean up
g_free(target_path);
} else {
fprintf(stderr, "ERROR: Icon file not extracted: %s", tmp_path);
}
}
void appimage_extract_file_following_symlinks(const gchar* appimage_file_path, const char* file_path,
const char* target_dir) {
appimage_handler handler = create_appimage_handler(appimage_file_path);
extract_appimage_file(&handler, file_path, target_dir);
}
void extract_appimage_file_name(void *handler_data, void *entry_data, void *user_data) {
appimage_handler *h = handler_data;
struct archive_entry *entry = entry_data;
GList **list = user_data;
char *filename = h->get_file_name(h, entry);
GList* ptr = g_list_find_custom (*list, filename, g_strcmp0);
if (ptr == NULL)
*list = g_list_append(*list, filename);
else
free(filename);
}
char** appimage_list_files(const char *path) {
GList *list = NULL;
appimage_handler handler = create_appimage_handler(path);
handler.traverse(&handler, extract_appimage_file_name, &list);
int n = g_list_length(list);
char **result = malloc(sizeof(char*) * (n+1) );
result[n] = NULL;
GList *itr = list;
for (int i = 0; i < n; i ++) {
result[i] = (char *) itr->data;
itr = itr->next;
}
g_list_free(list);
return result;
}
void appimage_string_list_free(char** list) {
for (char **ptr = list; ptr != NULL && *ptr != NULL; ptr ++)
free(*ptr);
free(list);
}